Skip to content

Commit ab67523

Browse files
authored
Added Aptos CCTP support (#774)
* skeleton code * testnet works * refactor move scripts * mainnet addys
1 parent 7e4aaa3 commit ab67523

File tree

14 files changed

+372
-15
lines changed

14 files changed

+372
-15
lines changed

core/base/src/constants/circle.ts

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const usdcContracts = [[
2020
["Base", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
2121
["Polygon", "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"],
2222
["Sui", "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"],
23+
["Aptos", "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b"]
2324
]], [
2425
"Testnet", [
2526
["Sepolia", "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"],
@@ -30,6 +31,7 @@ const usdcContracts = [[
3031
["BaseSepolia", "0x036CbD53842c5426634e7929541eC2318f3dCF7e"],
3132
["Polygon", "0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97"],
3233
["Sui", "0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC"],
34+
["Aptos", "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832"]
3335
]],
3436
] as const satisfies MapLevel<Network, MapLevel<Chain, string>>;
3537
export const usdcContract = constMap(usdcContracts);
@@ -46,6 +48,7 @@ const circleDomains = [[
4648
["Base", 6],
4749
["Polygon", 7],
4850
["Sui", 8],
51+
["Aptos", 9],
4952
]], [
5053
"Testnet", [
5154
["Sepolia", 0],
@@ -56,6 +59,7 @@ const circleDomains = [[
5659
["BaseSepolia", 6],
5760
["Polygon", 7],
5861
["Sui", 8],
62+
["Aptos", 9],
5963
]],
6064
] as const satisfies MapLevel<Network, MapLevel<Chain, number>>;
6165

core/base/src/constants/contracts/circle.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ export const circleContracts = [[
5959
messageTransmitter: "0x08d87d37ba49e785dde270a83f8e979605b03dc552b5548f26fdf2f49bf7ed1b",
6060
wormholeRelayer: "",
6161
wormhole: "",
62+
}], [
63+
"Aptos", {
64+
tokenMessenger: "0x9e6702a472080ea3caaf6ba9dfaa6effad2290a9ba9adaacd5af5c618e42782d",
65+
messageTransmitter: "0x177e17751820e4b4371873ca8c30279be63bdea63b88ed0f2239c2eea10f1772",
66+
wormholeRelayer: "",
67+
wormhole: "",
6268
}],
63-
]], [
69+
]], [
6470
"Testnet", [[
6571
"Sepolia", {
6672
tokenMessenger: "0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5",
@@ -109,6 +115,12 @@ export const circleContracts = [[
109115
messageTransmitter: "0x4931e06dce648b3931f890035bd196920770e913e43e45990b383f6486fdd0a5",
110116
wormholeRelayer: "",
111117
wormhole: "",
118+
}], [
119+
"Aptos", {
120+
tokenMessenger: "0x5f9b937419dda90aa06c1836b7847f65bbbe3f1217567758dc2488be31a477b9",
121+
messageTransmitter: "0x081e86cebf457a0c6004f35bd648a2794698f52e0dde09a48619dcd3d4cc23d9",
122+
wormholeRelayer: "",
123+
wormhole: "",
112124
}],
113-
]],
125+
]],
114126
] as const satisfies MapLevels<[Network, Chain, CircleContracts]>;

package-lock.json

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"platforms/aptos",
6363
"platforms/aptos/protocols/core",
6464
"platforms/aptos/protocols/tokenBridge",
65+
"platforms/aptos/protocols/cctp",
6566
"sdk",
6667
"examples"
6768
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"name": "@wormhole-foundation/sdk-aptos-cctp",
3+
"version": "1.0.3",
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/wormhole-foundation/wormhole-sdk-ts.git"
7+
},
8+
"bugs": {
9+
"url": "https://github.com/wormhole-foundation/wormhole-sdk-ts/issues"
10+
},
11+
"homepage": "https://github.com/wormhole-foundation/wormhole-sdk-ts#readme",
12+
"directories": {
13+
"test": "tests"
14+
},
15+
"license": "Apache-2.0",
16+
"main": "./dist/cjs/index.js",
17+
"types": "./dist/cjs/index.d.ts",
18+
"module": "./dist/esm/index.js",
19+
"description": "SDK for Aptos chains, used in conjunction with @wormhole-foundation/sdk",
20+
"files": [
21+
"dist/esm",
22+
"dist/cjs"
23+
],
24+
"keywords": [
25+
"wormhole",
26+
"sdk",
27+
"typescript",
28+
"connect",
29+
"aptos"
30+
],
31+
"engines": {
32+
"node": ">=16"
33+
},
34+
"sideEffects": [
35+
"./dist/cjs/index.js",
36+
"./dist/esm/index.js"
37+
],
38+
"scripts": {
39+
"build:cjs": "tsc -p ./tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
40+
"build:esm": "tsc -p ./tsconfig.esm.json",
41+
"build": "npm run build:esm && npm run build:cjs",
42+
"rebuild": "npm run clean && npm run build",
43+
"clean": "rm -rf ./dist && rm -rf ./.turbo",
44+
"lint": "npm run prettier && eslint --fix ./src --ext .ts",
45+
"prettier": "prettier --write ./src"
46+
},
47+
"dependencies": {
48+
"@aptos-labs/ts-sdk": "^1.33.1",
49+
"@wormhole-foundation/sdk-connect": "1.0.3",
50+
"@wormhole-foundation/sdk-aptos": "1.0.3"
51+
},
52+
"type": "module",
53+
"exports": {
54+
".": {
55+
"react-native": {
56+
"import": "./dist/esm/index.js",
57+
"require": "./dist/cjs/index.js",
58+
"types": "./dist/cjs/index.d.ts",
59+
"default": "./dist/cjs/index.js"
60+
},
61+
"import": {
62+
"types": "./dist/esm/index.d.ts",
63+
"default": "./dist/esm/index.js"
64+
},
65+
"require": {
66+
"types": "./dist/cjs/index.d.ts",
67+
"default": "./dist/cjs/index.js"
68+
},
69+
"default": {
70+
"types": "./dist/cjs/index.d.ts",
71+
"default": "./dist/cjs/index.js"
72+
}
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import {
2+
AccountAddress as AptosAccountAddress,
3+
Aptos,
4+
InputGenerateTransactionPayloadData,
5+
InputScriptData,
6+
MoveVector,
7+
U32,
8+
U64,
9+
UserTransactionResponse,
10+
} from "@aptos-labs/ts-sdk";
11+
import {
12+
AptosPlatform,
13+
AptosUnsignedTransaction,
14+
type AptosChains,
15+
} from "@wormhole-foundation/sdk-aptos";
16+
import type {
17+
AccountAddress,
18+
ChainAddress,
19+
ChainsConfig,
20+
Network,
21+
Platform,
22+
} from "@wormhole-foundation/sdk-connect";
23+
import {
24+
CircleBridge,
25+
CircleTransferMessage,
26+
circle,
27+
Contracts,
28+
encoding,
29+
keccak256,
30+
} from "@wormhole-foundation/sdk-connect";
31+
import { AptosCCTPMoveScripts, aptosCCTPMoveScripts } from "./moveScripts.js";
32+
33+
export class AptosCircleBridge<N extends Network, C extends AptosChains>
34+
implements CircleBridge<N, C>
35+
{
36+
readonly usdcId: string;
37+
readonly tokenMessengerId: string;
38+
readonly messageTransmitterId: string;
39+
readonly moveScripts: AptosCCTPMoveScripts;
40+
41+
constructor(
42+
readonly network: N,
43+
readonly chain: C,
44+
readonly provider: Aptos,
45+
readonly contracts: Contracts,
46+
) {
47+
if (network === "Devnet") throw new Error("CircleBridge not supported on Devnet");
48+
49+
const usdcId = circle.usdcContract.get(this.network, this.chain);
50+
if (!usdcId) {
51+
throw new Error(
52+
`No USDC contract configured for network=${this.network} chain=${this.chain}`,
53+
);
54+
}
55+
56+
if (!contracts.cctp?.tokenMessenger)
57+
throw new Error(`Circle Token Messenger contract for domain ${chain} not found`);
58+
59+
if (!contracts.cctp?.messageTransmitter)
60+
throw new Error(`Circle Message Transmitter contract for domain ${chain} not found`);
61+
62+
if (!aptosCCTPMoveScripts.has(network)) throw new Error("No Aptos CCTP move scripts found");
63+
64+
this.usdcId = usdcId;
65+
this.tokenMessengerId = contracts.cctp?.tokenMessenger;
66+
this.messageTransmitterId = contracts.cctp.messageTransmitter;
67+
this.moveScripts = aptosCCTPMoveScripts.get(network)!;
68+
}
69+
70+
async *transfer(
71+
sender: AccountAddress<C>,
72+
recipient: ChainAddress,
73+
amount: bigint,
74+
): AsyncGenerator<AptosUnsignedTransaction<N, C>> {
75+
const destinationDomain = new U32(circle.circleChainId.get(this.network, recipient.chain)!);
76+
const burnToken = AptosAccountAddress.from(this.usdcId);
77+
const mintRecipient = AptosAccountAddress.from(
78+
recipient.address.toUniversalAddress().toUint8Array(),
79+
);
80+
81+
const functionArguments = [new U64(amount), destinationDomain, mintRecipient, burnToken];
82+
83+
const txData: InputScriptData = {
84+
bytecode: this.moveScripts.depositForBurn,
85+
functionArguments,
86+
};
87+
88+
yield this.createUnsignedTx(txData, "Aptos.CircleBridge.Transfer");
89+
}
90+
91+
async isTransferCompleted(message: CircleBridge.Message): Promise<boolean> {
92+
const sourceBytes = new U32(message.sourceDomain).bcsToBytes();
93+
const nonceBytes = new U64(message.nonce).bcsToBytes();
94+
const hash = keccak256(new Uint8Array([...sourceBytes, "-".charCodeAt(0), ...nonceBytes]));
95+
const hashStr = encoding.hex.encode(hash);
96+
97+
const isNonceUsed = await this.provider.view<[boolean]>({
98+
payload: {
99+
function: `${this.messageTransmitterId}::message_transmitter::is_nonce_used`,
100+
functionArguments: [hashStr],
101+
},
102+
});
103+
104+
return isNonceUsed[0];
105+
}
106+
107+
async *redeem(
108+
sender: AccountAddress<C>,
109+
message: CircleBridge.Message,
110+
attestation: string,
111+
): AsyncGenerator<AptosUnsignedTransaction<N, C>> {
112+
const functionArguments = [
113+
MoveVector.U8(CircleBridge.serialize(message)),
114+
MoveVector.U8(encoding.hex.decode(attestation)),
115+
];
116+
117+
const txData: InputScriptData = {
118+
bytecode: this.moveScripts.handleReceiveMessage,
119+
functionArguments,
120+
};
121+
122+
yield this.createUnsignedTx(txData, "Aptos.CircleBridge.Redeem");
123+
}
124+
125+
async parseTransactionDetails(digest: string): Promise<CircleTransferMessage> {
126+
const tx = await this.provider.getTransactionByHash({ transactionHash: digest });
127+
128+
const messageTransmitterId = this.messageTransmitterId.replace(/^0x0+/, "0x"); // remove any leading zeros to match event
129+
const circleMessageSentEvent = (tx as UserTransactionResponse).events?.find(
130+
(e) => e.type === `${messageTransmitterId}::message_transmitter::MessageSent`,
131+
);
132+
133+
if (!circleMessageSentEvent) {
134+
throw new Error("No MessageSent event found");
135+
}
136+
137+
const circleMessage = encoding.hex.decode(circleMessageSentEvent.data.message);
138+
139+
const [msg, hash] = CircleBridge.deserialize(circleMessage);
140+
const { payload } = msg;
141+
142+
const xferSender = payload.messageSender;
143+
const xferReceiver = payload.mintRecipient;
144+
145+
const sendChain = circle.toCircleChain(this.network, msg.sourceDomain);
146+
const rcvChain = circle.toCircleChain(this.network, msg.destinationDomain);
147+
148+
const token = { chain: sendChain, address: payload.burnToken };
149+
150+
return {
151+
from: { chain: sendChain, address: xferSender },
152+
to: { chain: rcvChain, address: xferReceiver },
153+
token: token,
154+
amount: payload.amount,
155+
message: msg,
156+
id: { hash },
157+
};
158+
}
159+
160+
static async fromRpc<N extends Network>(
161+
provider: Aptos,
162+
config: ChainsConfig<N, Platform>,
163+
): Promise<AptosCircleBridge<N, AptosChains>> {
164+
const [network, chain] = await AptosPlatform.chainFromRpc(provider);
165+
const conf = config[chain]!;
166+
if (conf.network !== network) {
167+
throw new Error(`Network mismatch: ${conf.network} != ${network}`);
168+
}
169+
170+
return new AptosCircleBridge(network as N, chain, provider, conf.contracts);
171+
}
172+
173+
private createUnsignedTx(
174+
txReq: InputGenerateTransactionPayloadData,
175+
description: string,
176+
parallelizable: boolean = false,
177+
): AptosUnsignedTransaction<N, C> {
178+
return new AptosUnsignedTransaction(
179+
txReq,
180+
this.network,
181+
this.chain,
182+
description,
183+
parallelizable,
184+
);
185+
}
186+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { registerProtocol } from "@wormhole-foundation/sdk-connect";
2+
3+
import { AptosCircleBridge } from "./circleBridge.js";
4+
5+
registerProtocol("Aptos", "CircleBridge", AptosCircleBridge);
6+
export * from "./circleBridge.js";

0 commit comments

Comments
 (0)