diff --git a/clients/js/Makefile b/clients/js/Makefile index 98bb6ed9b..e0cbdd9ed 100644 --- a/clients/js/Makefile +++ b/clients/js/Makefile @@ -23,5 +23,9 @@ install: build chmod +x build/main.js npm link +.PHONY: test +test: build + ./run_parse_tests + clean: rm -rf build node_modules diff --git a/clients/js/evm.ts b/clients/js/evm.ts index 827d06128..2c9b1a9c7 100644 --- a/clients/js/evm.ts +++ b/clients/js/evm.ts @@ -221,6 +221,10 @@ export async function execute_evm( console.log("Registering chain") console.log("Hash: " + (await nb.registerChain(vaa, overrides)).hash) break + case "Transfer": + console.log("Completing transfer") + console.log("Hash: " + (await nb.completeTransfer(vaa, overrides)).hash) + break default: impossible(payload) diff --git a/clients/js/parse_tests/nft-bridge-transfer-1.expected b/clients/js/parse_tests/nft-bridge-transfer-1.expected new file mode 100644 index 000000000..12c8b4be4 --- /dev/null +++ b/clients/js/parse_tests/nft-bridge-transfer-1.expected @@ -0,0 +1,24 @@ +{ + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 1, + nonce: 1, + emitterChain: 1, + emitterAddress: '0x0000000000000000000000000000000000000000000000000000000000000004', + sequence: 41401099n, + consistencyLevel: 0, + payload: { + module: 'NFTBridge', + type: 'Transfer', + tokenAddress: '0x0000000000000000000000000000000000000000000000000000000000000004', + tokenChain: 1, + tokenSymbol: 'FOO', + tokenName: 'BAR', + tokenId: 10n, + tokenURI: 'google.com', + toAddress: '0x0000000000000000000000000000000000000000000000000000000000000004', + chain: 10 + }, + digest: '0xe2a7a0f5d67018c74683851fca870403455f89d9f474e2af104740e31a00da63' +} diff --git a/clients/js/parse_tests/nft-bridge-transfer-1.test b/clients/js/parse_tests/nft-bridge-transfer-1.test new file mode 100644 index 000000000..fd49cec9a --- /dev/null +++ b/clients/js/parse_tests/nft-bridge-transfer-1.test @@ -0,0 +1 @@ +010000000000000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000277bb0b0001000000000000000000000000000000000000000000000000000000000000000400010000000000000000000000000000000000000000000000000000000000464f4f0000000000000000000000000000000000000000000000000000000000424152000000000000000000000000000000000000000000000000000000000000000a0a676f6f676c652e636f6d0000000000000000000000000000000000000000000000000000000000000004000a \ No newline at end of file diff --git a/clients/js/run_parse_tests b/clients/js/run_parse_tests index 3d5088385..f413193f8 100755 --- a/clients/js/run_parse_tests +++ b/clients/js/run_parse_tests @@ -42,7 +42,7 @@ for test in ${test_files[@]}; do expected="$test_name.expected" result=$(mktemp) - worm parse $(cat "$test") > "$result" 2>&1 + node build/main.js parse $(cat "$test") > "$result" 2>&1 if [ $accept = true ]; then echo "Updating $test_name" cat "$result" > "$expected" diff --git a/clients/js/solana.ts b/clients/js/solana.ts index 90e91fe99..f10ef20e3 100644 --- a/clients/js/solana.ts +++ b/clients/js/solana.ts @@ -46,9 +46,11 @@ export async function execute_solana( console.log("Registering chain") ix = nft_bridge.register_chain_ix(nft_bridge_id.toString(), bridge_id.toString(), from.publicKey.toString(), vaa); break + case "Transfer": + throw Error("Can't redeem NFTs from CLI") + // TODO: what's the authority account? just bail for now default: ix = impossible(v.payload) - } break case "TokenBridge": diff --git a/clients/js/terra.ts b/clients/js/terra.ts index 71d0410ad..d40b227b3 100644 --- a/clients/js/terra.ts +++ b/clients/js/terra.ts @@ -72,6 +72,9 @@ export async function execute_terra( case "RegisterChain": console.log("Registering chain"); break; + case "Transfer": + console.log("Completing transfer"); + break; default: impossible(payload); } diff --git a/clients/js/vaa.ts b/clients/js/vaa.ts index 7fd57984e..5b523296c 100644 --- a/clients/js/vaa.ts +++ b/clients/js/vaa.ts @@ -63,7 +63,7 @@ export type Payload = | TokenBridgeTransfer | TokenBridgeTransferWithPayload | TokenBridgeAttestMeta -// TODO: add other types of payloads + | NFTBridgeTransfer export type ContractUpgrade = CoreContractUpgrade @@ -81,7 +81,9 @@ export function parse(buffer: Buffer): VAA { .or(tokenBridgeTransferParser()) .or(tokenBridgeTransferWithPayloadParser()) .or(tokenBridgeAttestMetaParser()) + .or(nftBridgeTransferParser()) const payload = parser.parse(vaa.payload) + delete payload['tokenURILength'] var myVAA = { ...vaa, payload } return myVAA @@ -188,6 +190,9 @@ function vaaBody(vaa: VAA) { case "RegisterChain": payload_str = serialisePortalRegisterChain(payload) break + case "Transfer": + payload_str = serialiseNFTBridgeTransfer(payload) + break default: impossible(payload) break @@ -650,6 +655,88 @@ function serialiseTokenBridgeTransferWithPayload(payload: TokenBridgeTransferWit //////////////////////////////////////////////////////////////////////////////// // NFT bridge +export interface NFTBridgeTransfer { + module: "NFTBridge" + type: "Transfer" + tokenAddress: string + tokenChain: number + tokenSymbol: string + tokenName: string + tokenId: bigint + tokenURI: string + toAddress: string + chain: number +} + +function nftBridgeTransferParser(): P { + return new P(new Parser() + .endianess("big") + .string("module", { + length: (_) => 0, + formatter: (_) => "NFTBridge" + }) + .uint8("type", { + assert: 1, + formatter: (_action) => "Transfer" + }) + .array("tokenAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") + }) + .uint16("tokenChain") + .array("tokenSymbol", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) + }) + .array("tokenName", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) + }) + .array("tokenId", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt() + }) + .uint8("tokenURILength") + .array("tokenURI", { + type: "uint8", + lengthInBytes: function() { + return this.tokenURILength + }, + formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8") + }) + .array("toAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") + }) + .uint16("chain") + .string("end", { + greedy: true, + assert: str => str === "" + }) + ) +} + +function serialiseNFTBridgeTransfer(payload: NFTBridgeTransfer): string { + const body = [ + encode("uint8", 1), + encode("bytes32", hex(payload.tokenAddress)), + encode("uint16", payload.tokenChain), + encode("bytes32", encodeString(payload.tokenSymbol)), + encode("bytes32", encodeString(payload.tokenName)), + encode("uint256", payload.tokenId), + encode("uint8", payload.tokenURI.length), + Buffer.from(payload.tokenURI, "utf8").toString("hex"), + encode("bytes32", hex(payload.toAddress)), + encode("uint16", payload.chain), + ] + return body.join("") +} + // This function should be called after pattern matching on all possible options // of an enum (union) type, so that typescript can derive that no other options // are possible. If (from JavaScript land) an unsupported argument is passed