From e109024e9920ea9d33623bd681e15648c224f136 Mon Sep 17 00:00:00 2001 From: A5 Pickle <5342825+a5-pickle@users.noreply.github.com> Date: Wed, 26 Oct 2022 09:28:46 -0500 Subject: [PATCH] sdk/js: Remove Solana WASM Dependencies (#1375) * sdk/js: Remove Solana WASM Dependencies * sdk/js: Uptick @solana/web3.js and @solana/spl-token versions * solana: Add IDLs for Wormhole, Token Bridge and NFT Bridge * sdk/js: Remove Solana WASM to Use IDL Coders * sdk/js: Remove src/migration * sdk/js: Add CPI Account Getters * testing: Add solana-test-validator SDK tests * sdk/js: add CHANGELOG.md * sdk/js: remove await * sdk/js: fix getIsTransferCompletedAptos * sdk/js: aptos integration test waits for tx * sdk/js: remove commented out code * sdk/js: fix inferred type Co-authored-by: A5 Pickle Co-authored-by: Evan Gray --- sdk/js/.gitignore | 6 +- sdk/js/CHANGELOG.md | 21 + sdk/js/package-lock.json | 705 +++--- sdk/js/package.json | 12 +- sdk/js/scripts/compileAnchorIdls.js | 42 + sdk/js/src/algorand/__tests__/unit.ts | 3 - sdk/js/src/bridge/getClaimAddress.ts | 18 +- sdk/js/src/bridge/getEmitterAddress.ts | 13 +- sdk/js/src/bridge/getSignedVAAHash.ts | 20 +- sdk/js/src/cosmwasm/query.testnet.test.ts | 1 - sdk/js/src/index.ts | 7 +- sdk/js/src/migration/addLiquidity.ts | 40 - sdk/js/src/migration/authorityAddress.ts | 6 - sdk/js/src/migration/claimShares.ts | 40 - sdk/js/src/migration/createPool.ts | 20 - sdk/js/src/migration/fromCustodyAddress.ts | 9 - sdk/js/src/migration/migrateTokens.ts | 40 - sdk/js/src/migration/parsePool.ts | 6 - sdk/js/src/migration/poolAddress.ts | 10 - sdk/js/src/migration/removeLiquidity.ts | 40 - sdk/js/src/migration/shareMintAddress.ts | 9 - sdk/js/src/migration/toCustodyAddress.ts | 9 - sdk/js/src/mock/governance.ts | 205 ++ sdk/js/src/mock/index.ts | 5 + sdk/js/src/mock/misc.ts | 17 + sdk/js/src/mock/nftBridge.ts | 87 + sdk/js/src/mock/tokenBridge.ts | 209 ++ sdk/js/src/mock/wormhole.ts | 129 ++ .../src/nft_bridge/__tests__/integration.ts | 22 +- sdk/js/src/nft_bridge/getForeignAsset.ts | 46 +- .../src/nft_bridge/getIsTransferCompleted.ts | 30 +- sdk/js/src/nft_bridge/getIsWrappedAsset.ts | 39 +- sdk/js/src/nft_bridge/getOriginalAsset.ts | 87 +- sdk/js/src/nft_bridge/redeem.ts | 112 +- sdk/js/src/nft_bridge/transfer.ts | 152 +- sdk/js/src/solana/anchor/common.ts | 97 + sdk/js/src/solana/anchor/error.ts | 10 + sdk/js/src/solana/anchor/idl.ts | 204 ++ sdk/js/src/solana/anchor/index.ts | 3 + sdk/js/src/solana/getBridgeFeeIx.ts | 25 - sdk/js/src/solana/index.ts | 18 +- .../src/solana/nftBridge/accounts/config.ts | 18 + sdk/js/src/solana/nftBridge/accounts/index.ts | 13 + .../src/solana/nftBridge/accounts/wrapped.ts | 83 + sdk/js/src/solana/nftBridge/coder/accounts.ts | 40 + sdk/js/src/solana/nftBridge/coder/events.ts | 12 + sdk/js/src/solana/nftBridge/coder/index.ts | 24 + .../src/solana/nftBridge/coder/instruction.ts | 130 ++ sdk/js/src/solana/nftBridge/coder/state.ts | 12 + sdk/js/src/solana/nftBridge/coder/types.ts | 12 + sdk/js/src/solana/nftBridge/index.ts | 3 + .../solana/nftBridge/instructions/approve.ts | 15 + .../nftBridge/instructions/completeNative.ts | 107 + .../nftBridge/instructions/completeWrapped.ts | 117 + .../instructions/completeWrappedMeta.ts | 103 + .../nftBridge/instructions/governance.ts | 161 ++ .../solana/nftBridge/instructions/index.ts | 8 + .../nftBridge/instructions/initialize.ts | 47 + .../nftBridge/instructions/transferNative.ts | 122 + .../nftBridge/instructions/transferWrapped.ts | 137 ++ sdk/js/src/solana/nftBridge/program.ts | 43 + sdk/js/src/solana/postVaa.ts | 212 -- sdk/js/src/solana/rust.ts | 23 - sdk/js/src/solana/sendAndConfirmPostVaa.ts | 171 ++ .../src/solana/tokenBridge/accounts/config.ts | 42 + .../solana/tokenBridge/accounts/custody.ts | 9 + .../solana/tokenBridge/accounts/endpoint.ts | 70 + .../src/solana/tokenBridge/accounts/index.ts | 7 + .../src/solana/tokenBridge/accounts/signer.ts | 20 + .../accounts/transferWithPayload.ts | 14 + .../solana/tokenBridge/accounts/wrapped.ts | 87 + .../src/solana/tokenBridge/coder/accounts.ts | 40 + sdk/js/src/solana/tokenBridge/coder/events.ts | 12 + sdk/js/src/solana/tokenBridge/coder/index.ts | 24 + .../solana/tokenBridge/coder/instruction.ts | 254 +++ sdk/js/src/solana/tokenBridge/coder/state.ts | 12 + sdk/js/src/solana/tokenBridge/coder/types.ts | 12 + sdk/js/src/solana/tokenBridge/cpi.ts | 477 ++++ sdk/js/src/solana/tokenBridge/index.ts | 4 + .../tokenBridge/instructions/approve.ts | 17 + .../tokenBridge/instructions/attestToken.ts | 97 + .../instructions/completeNative.ts | 105 + .../instructions/completeWrapped.ts | 110 + .../tokenBridge/instructions/createWrapped.ts | 107 + .../tokenBridge/instructions/governance.ts | 161 ++ .../solana/tokenBridge/instructions/index.ts | 11 + .../tokenBridge/instructions/initialize.ts | 47 + .../instructions/transferNative.ts | 118 + .../instructions/transferNativeWithPayload.ts | 125 ++ .../instructions/transferWrapped.ts | 129 ++ .../transferWrappedWithPayload.ts | 136 ++ sdk/js/src/solana/tokenBridge/program.ts | 33 + sdk/js/src/solana/utils/account.ts | 69 + .../src/solana/utils/bpfLoaderUpgradeable.ts | 23 + sdk/js/src/solana/utils/connection.ts | 12 + sdk/js/src/solana/utils/index.ts | 6 + sdk/js/src/solana/utils/secp256k1.ts | 124 ++ sdk/js/src/solana/utils/splMetadata.ts | 331 +++ sdk/js/src/solana/utils/transaction.ts | 208 ++ sdk/js/src/solana/wasm.ts | 1 - sdk/js/src/solana/wormhole/accounts/claim.ts | 54 + sdk/js/src/solana/wormhole/accounts/config.ts | 68 + .../src/solana/wormhole/accounts/emitter.ts | 34 + .../solana/wormhole/accounts/feeCollector.ts | 8 + .../solana/wormhole/accounts/guardianSet.ts | 73 + sdk/js/src/solana/wormhole/accounts/index.ts | 8 + .../src/solana/wormhole/accounts/postedVaa.ts | 50 + .../solana/wormhole/accounts/signatureSet.ts | 40 + .../src/solana/wormhole/accounts/upgrade.ts | 8 + sdk/js/src/solana/wormhole/coder/accounts.ts | 78 + sdk/js/src/solana/wormhole/coder/events.ts | 10 + sdk/js/src/solana/wormhole/coder/index.ts | 24 + .../src/solana/wormhole/coder/instruction.ts | 154 ++ sdk/js/src/solana/wormhole/coder/state.ts | 12 + sdk/js/src/solana/wormhole/coder/types.ts | 12 + sdk/js/src/solana/wormhole/cpi.ts | 92 + sdk/js/src/solana/wormhole/index.ts | 5 + .../wormhole/instructions/feeTransfer.ts | 27 + .../wormhole/instructions/governance.ts | 262 +++ .../src/solana/wormhole/instructions/index.ts | 6 + .../wormhole/instructions/initialize.ts | 64 + .../wormhole/instructions/postMessage.ts | 48 + .../solana/wormhole/instructions/postVaa.ts | 98 + .../wormhole/instructions/verifySignature.ts | 172 ++ sdk/js/src/solana/wormhole/message.ts | 66 + sdk/js/src/solana/wormhole/program.ts | 40 + .../__tests__/algorand-integration.ts | 5 +- .../__tests__/aptos-integration.ts | 28 +- .../token_bridge/__tests__/eth-integration.ts | 46 +- .../__tests__/near-integration.ts | 3 - .../__tests__/solana-integration.ts | 11 +- .../__tests__/terra-integration.ts | 9 +- sdk/js/src/token_bridge/attest.ts | 48 +- sdk/js/src/token_bridge/createWrapped.ts | 48 +- sdk/js/src/token_bridge/getForeignAsset.ts | 55 +- .../token_bridge/getIsTransferCompleted.ts | 67 +- sdk/js/src/token_bridge/getIsWrappedAsset.ts | 41 +- sdk/js/src/token_bridge/getOriginalAsset.ts | 78 +- sdk/js/src/token_bridge/redeem.ts | 183 +- sdk/js/src/token_bridge/transfer.ts | 282 +-- sdk/js/src/utils/index.ts | 1 + sdk/js/src/utils/keccak.ts | 5 + sdk/js/src/utils/parseVaa.ts | 65 +- sdk/js/src/utils/solana.ts | 58 - sdk/js/src/vaa/generic.ts | 872 ++++++++ sdk/js/src/vaa/governance.ts | 31 + sdk/js/src/vaa/index.ts | 5 + sdk/js/src/vaa/nftBridge.ts | 119 + sdk/js/src/vaa/tokenBridge.ts | 177 ++ sdk/js/src/vaa/wormhole.ts | 56 + sdk/js/tsconfig.json | 1 + solana/idl/nft_bridge.json | 621 ++++++ solana/idl/token_bridge.json | 947 ++++++++ solana/idl/wormhole.json | 624 ++++++ testing/Dockerfile.sdk.test | 2 + testing/solana-test-validator/.gitignore | 3 + testing/solana-test-validator/Makefile | 21 + testing/solana-test-validator/package.json | 28 + .../solana-test-validator/run_sdk_tests.sh | 38 + .../sdk-tests/0_deploy_and_upgrade.ts | 361 +++ .../sdk-tests/1_wormhole.ts | 760 +++++++ .../sdk-tests/2_token_bridge.ts | 1982 +++++++++++++++++ .../sdk-tests/3_nft_bridge.ts | 1290 +++++++++++ .../sdk-tests/helpers/consts.ts | 52 + .../sdk-tests/helpers/utils.ts | 77 + ...V7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC.json | 1 + ...hmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa.json | 1 + ...gev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q.json | 1 + ...bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json | 1 + testing/solana-test-validator/tsconfig.json | 13 + testing/solana-test-validator/yarn.lock | 1504 +++++++++++++ 171 files changed, 17786 insertions(+), 1589 deletions(-) create mode 100644 sdk/js/scripts/compileAnchorIdls.js delete mode 100644 sdk/js/src/migration/addLiquidity.ts delete mode 100644 sdk/js/src/migration/authorityAddress.ts delete mode 100644 sdk/js/src/migration/claimShares.ts delete mode 100644 sdk/js/src/migration/createPool.ts delete mode 100644 sdk/js/src/migration/fromCustodyAddress.ts delete mode 100644 sdk/js/src/migration/migrateTokens.ts delete mode 100644 sdk/js/src/migration/parsePool.ts delete mode 100644 sdk/js/src/migration/poolAddress.ts delete mode 100644 sdk/js/src/migration/removeLiquidity.ts delete mode 100644 sdk/js/src/migration/shareMintAddress.ts delete mode 100644 sdk/js/src/migration/toCustodyAddress.ts create mode 100644 sdk/js/src/mock/governance.ts create mode 100644 sdk/js/src/mock/index.ts create mode 100644 sdk/js/src/mock/misc.ts create mode 100644 sdk/js/src/mock/nftBridge.ts create mode 100644 sdk/js/src/mock/tokenBridge.ts create mode 100644 sdk/js/src/mock/wormhole.ts create mode 100644 sdk/js/src/solana/anchor/common.ts create mode 100644 sdk/js/src/solana/anchor/error.ts create mode 100644 sdk/js/src/solana/anchor/idl.ts create mode 100644 sdk/js/src/solana/anchor/index.ts delete mode 100644 sdk/js/src/solana/getBridgeFeeIx.ts create mode 100644 sdk/js/src/solana/nftBridge/accounts/config.ts create mode 100644 sdk/js/src/solana/nftBridge/accounts/index.ts create mode 100644 sdk/js/src/solana/nftBridge/accounts/wrapped.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/accounts.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/events.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/index.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/instruction.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/state.ts create mode 100644 sdk/js/src/solana/nftBridge/coder/types.ts create mode 100644 sdk/js/src/solana/nftBridge/index.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/approve.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/completeNative.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/governance.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/index.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/initialize.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/transferNative.ts create mode 100644 sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts create mode 100644 sdk/js/src/solana/nftBridge/program.ts delete mode 100644 sdk/js/src/solana/postVaa.ts delete mode 100644 sdk/js/src/solana/rust.ts create mode 100644 sdk/js/src/solana/sendAndConfirmPostVaa.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/config.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/custody.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/endpoint.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/index.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/signer.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts create mode 100644 sdk/js/src/solana/tokenBridge/accounts/wrapped.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/accounts.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/events.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/index.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/instruction.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/state.ts create mode 100644 sdk/js/src/solana/tokenBridge/coder/types.ts create mode 100644 sdk/js/src/solana/tokenBridge/cpi.ts create mode 100644 sdk/js/src/solana/tokenBridge/index.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/approve.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/attestToken.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/completeNative.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/governance.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/index.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/initialize.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/transferNative.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts create mode 100644 sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts create mode 100644 sdk/js/src/solana/tokenBridge/program.ts create mode 100644 sdk/js/src/solana/utils/account.ts create mode 100644 sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts create mode 100644 sdk/js/src/solana/utils/connection.ts create mode 100644 sdk/js/src/solana/utils/index.ts create mode 100644 sdk/js/src/solana/utils/secp256k1.ts create mode 100644 sdk/js/src/solana/utils/splMetadata.ts create mode 100644 sdk/js/src/solana/utils/transaction.ts delete mode 100644 sdk/js/src/solana/wasm.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/claim.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/config.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/emitter.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/feeCollector.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/guardianSet.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/index.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/postedVaa.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/signatureSet.ts create mode 100644 sdk/js/src/solana/wormhole/accounts/upgrade.ts create mode 100644 sdk/js/src/solana/wormhole/coder/accounts.ts create mode 100644 sdk/js/src/solana/wormhole/coder/events.ts create mode 100644 sdk/js/src/solana/wormhole/coder/index.ts create mode 100644 sdk/js/src/solana/wormhole/coder/instruction.ts create mode 100644 sdk/js/src/solana/wormhole/coder/state.ts create mode 100644 sdk/js/src/solana/wormhole/coder/types.ts create mode 100644 sdk/js/src/solana/wormhole/cpi.ts create mode 100644 sdk/js/src/solana/wormhole/index.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/feeTransfer.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/governance.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/index.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/initialize.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/postMessage.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/postVaa.ts create mode 100644 sdk/js/src/solana/wormhole/instructions/verifySignature.ts create mode 100644 sdk/js/src/solana/wormhole/message.ts create mode 100644 sdk/js/src/solana/wormhole/program.ts create mode 100644 sdk/js/src/utils/keccak.ts delete mode 100644 sdk/js/src/utils/solana.ts create mode 100644 sdk/js/src/vaa/generic.ts create mode 100644 sdk/js/src/vaa/governance.ts create mode 100644 sdk/js/src/vaa/index.ts create mode 100644 sdk/js/src/vaa/nftBridge.ts create mode 100644 sdk/js/src/vaa/tokenBridge.ts create mode 100644 sdk/js/src/vaa/wormhole.ts create mode 100644 solana/idl/nft_bridge.json create mode 100644 solana/idl/token_bridge.json create mode 100644 solana/idl/wormhole.json create mode 100644 testing/solana-test-validator/.gitignore create mode 100644 testing/solana-test-validator/Makefile create mode 100644 testing/solana-test-validator/package.json create mode 100755 testing/solana-test-validator/run_sdk_tests.sh create mode 100644 testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts create mode 100644 testing/solana-test-validator/sdk-tests/1_wormhole.ts create mode 100644 testing/solana-test-validator/sdk-tests/2_token_bridge.ts create mode 100644 testing/solana-test-validator/sdk-tests/3_nft_bridge.ts create mode 100644 testing/solana-test-validator/sdk-tests/helpers/consts.ts create mode 100644 testing/solana-test-validator/sdk-tests/helpers/utils.ts create mode 100644 testing/solana-test-validator/sdk-tests/keys/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC.json create mode 100644 testing/solana-test-validator/sdk-tests/keys/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa.json create mode 100644 testing/solana-test-validator/sdk-tests/keys/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q.json create mode 100644 testing/solana-test-validator/sdk-tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json create mode 100644 testing/solana-test-validator/tsconfig.json create mode 100644 testing/solana-test-validator/yarn.lock diff --git a/sdk/js/.gitignore b/sdk/js/.gitignore index 6135eddca..ecc1cbe14 100644 --- a/sdk/js/.gitignore +++ b/sdk/js/.gitignore @@ -30,4 +30,8 @@ yarn-error.log* /src/proto # build -/lib \ No newline at end of file +/lib + +# solana idl +/src/anchor-idl +/src/solana/types diff --git a/sdk/js/CHANGELOG.md b/sdk/js/CHANGELOG.md index caba0822a..24f061af3 100644 --- a/sdk/js/CHANGELOG.md +++ b/sdk/js/CHANGELOG.md @@ -2,10 +2,30 @@ ## 0.9.0 +### Added + +Methods to create transaction instructions for Wormhole (Core Bridge), Token Bridge and NFT Bridge + +Methods to generate PDAs for Wormhole (Core Bridge), Token Bridge and NFT Bridge + +Methods to deserialize account data for Wormhole (Core Bridge), Token Bridge and NFT Bridge + +Other Solana utility objects and methods + +VAA (Verified Wormhole Message) deserializers + +Optional Confirmation arguments for account retrieval and wherever they are relevant + +Mock objects to be used in local integration tests (e.g. MockGuardians) + ### Changed Use FQTs in Aptos SDK +### Removed + +Dependency: @certusone/wormhole-sdk-wasm + ## 0.8.0 ### Added @@ -17,6 +37,7 @@ Aptos support Wormchain rename ## 0.7.2 + ### Added XPLA mainnet support and functions diff --git a/sdk/js/package-lock.json b/sdk/js/package-lock.json index 0fe17ae67..53baa8610 100644 --- a/sdk/js/package-lock.json +++ b/sdk/js/package-lock.json @@ -12,14 +12,17 @@ "@certusone/wormhole-sdk-proto-web": "^0.0.5", "@certusone/wormhole-sdk-wasm": "^0.0.1", "@injectivelabs/sdk-ts": "^1.0.75", - "@solana/spl-token": "^0.1.8", - "@solana/web3.js": "^1.24.0", + "@project-serum/anchor": "^0.25.0", + "@solana/spl-token": "^0.3.5", + "@solana/web3.js": "^1.66.2", "@terra-money/terra.js": "^3.1.3", "@xpla/xpla.js": "^0.2.1", "algosdk": "^1.15.0", "aptos": "^1.3.16", "axios": "^0.24.0", "bech32": "^2.0.0", + "binary-parser": "^2.2.1", + "elliptic": "^6.5.4", "js-base64": "^3.6.1", "near-api-js": "^1.0.0" }, @@ -29,9 +32,11 @@ "@injectivelabs/tx-ts": "^1.0.22", "@openzeppelin/contracts": "^4.2.0", "@typechain/ethers-v5": "^7.0.1", + "@types/elliptic": "^6.4.14", "@types/jest": "^27.0.2", "@types/long": "^4.0.1", "@types/node": "^16.6.1", + "@types/node-fetch": "^2.6.2", "@types/react": "^17.0.19", "copy-dir": "^1.3.0", "ethers": "^5.6.8", @@ -2496,10 +2501,26 @@ "rlp": "^2.2.3" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", + "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@noble/hashes": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz", - "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==", + "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==" + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz", + "integrity": "sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==", "funding": [ { "type": "individual", @@ -2514,6 +2535,51 @@ "dev": true, "license": "MIT" }, + "node_modules/@project-serum/anchor": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.25.0.tgz", + "integrity": "sha512-E6A5Y/ijqpfMJ5psJvbw0kVTzLZFUcOFgs6eSM2M2iWE1lVRF18T6hWZVNl6zqZsoz98jgnNHtVGJMs+ds9A7A==", + "dependencies": { + "@project-serum/borsh": "^0.2.5", + "@solana/web3.js": "^1.36.0", + "base64-js": "^1.5.1", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^5.3.1", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "js-sha256": "^0.9.0", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "node_modules/@project-serum/anchor/node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" + }, + "node_modules/@project-serum/borsh": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.5.tgz", + "integrity": "sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.2.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2631,9 +2697,9 @@ } }, "node_modules/@solana/buffer-layout": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz", - "integrity": "sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz", + "integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==", "dependencies": { "buffer": "~6.0.3" }, @@ -2641,6 +2707,20 @@ "node": ">=5.10" } }, + "node_modules/@solana/buffer-layout-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", + "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/@solana/buffer-layout/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -2665,19 +2745,19 @@ } }, "node_modules/@solana/spl-token": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz", - "integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.5.tgz", + "integrity": "sha512-0bGC6n415lGjKu02gkLOIpP1wzndSP0SHwN9PefJ+wKAhmfU1rl3AV1Pa41uap2kzSCD6F9642ngNO8KXPvh/g==", "dependencies": { - "@babel/runtime": "^7.10.5", - "@solana/web3.js": "^1.21.0", - "bn.js": "^5.1.0", - "buffer": "6.0.3", - "buffer-layout": "^1.2.0", - "dotenv": "10.0.0" + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "buffer": "^6.0.3" }, "engines": { - "node": ">= 10" + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.47.4" } }, "node_modules/@solana/spl-token/node_modules/buffer": { @@ -2704,24 +2784,28 @@ } }, "node_modules/@solana/web3.js": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.24.0.tgz", - "integrity": "sha512-Br3r2YMoM6Ia7NlWVpe+w/cFlRMfW1yXCxy19rxjKZbxIb1i/iEGSOPGsEGCD6FgHJgyWGzD2tf4P1tWra5Fxg==", + "version": "1.66.2", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.66.2.tgz", + "integrity": "sha512-RyaHMR2jGmaesnYP045VLeBGfR/gAW3cvZHzMFGg7bkO+WOYOYp1nEllf0/la4U4qsYGKCsO9eEevR5fhHiVHg==", "dependencies": { "@babel/runtime": "^7.12.5", - "@solana/buffer-layout": "^3.0.0", + "@noble/ed25519": "^1.7.0", + "@noble/hashes": "^1.1.2", + "@noble/secp256k1": "^1.6.3", + "@solana/buffer-layout": "^4.0.0", + "bigint-buffer": "^1.1.5", "bn.js": "^5.0.0", - "borsh": "^0.4.0", + "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.1", - "crypto-hash": "^1.2.2", + "fast-stable-stringify": "^1.0.0", "jayson": "^3.4.4", - "js-sha3": "^0.8.0", - "node-fetch": "^2.6.1", - "rpc-websockets": "^7.4.2", - "secp256k1": "^4.0.2", - "superstruct": "^0.14.2", - "tweetnacl": "^1.0.0" + "node-fetch": "2", + "rpc-websockets": "^7.5.0", + "superstruct": "^0.14.2" + }, + "engines": { + "node": ">=12.20.0" } }, "node_modules/@szmarczak/http-timer": { @@ -2871,6 +2955,15 @@ "@types/node": "*" } }, + "node_modules/@types/elliptic": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", + "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", + "dev": true, + "dependencies": { + "@types/bn.js": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", @@ -2952,6 +3045,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -3156,16 +3259,6 @@ "node": ">=10.0.0" } }, - "node_modules/101": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/101/-/101-1.6.3.tgz", - "integrity": "sha512-4dmQ45yY0Dx24Qxp+zAsNLlMF6tteCyfVzgbulvSyC7tCyd3V8sW76sS0tHq8NpcbXfWTKasfyfzU1Kd86oKzw==", - "dependencies": { - "clone": "^1.0.2", - "deep-eql": "^0.1.3", - "keypather": "^1.10.2" - } - }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -3471,19 +3564,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/assert-args": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/assert-args/-/assert-args-1.2.1.tgz", - "integrity": "sha1-QEEDoUUqMv53iYgR5U5ZCoqTc70=", - "dependencies": { - "101": "^1.2.0", - "compound-subject": "0.0.1", - "debug": "^2.2.0", - "get-prototype-of": "0.0.0", - "is-capitalized": "^1.0.0", - "is-class": "0.0.4" - } - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3786,6 +3866,18 @@ "node": ">=0.6" } }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bignumber.js": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", @@ -3794,6 +3886,14 @@ "node": "*" } }, + "node_modules/binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", + "engines": { + "node": ">=12" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3887,12 +3987,11 @@ } }, "node_modules/borsh": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz", - "integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", "dependencies": { - "@types/bn.js": "^4.11.5", - "bn.js": "^5.0.0", + "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } @@ -4202,7 +4301,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -4324,12 +4422,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/circular-json": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", - "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==", - "deprecated": "CircularJSON is in maintenance only, flatted is its successor." - }, "node_modules/cjs-module-lexer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", @@ -4352,14 +4444,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -4434,11 +4518,6 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, - "node_modules/compound-subject": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/compound-subject/-/compound-subject-0.0.1.tgz", - "integrity": "sha1-JxVUaYoVrmCLHfyv0wt7oeqJLEs=" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4630,6 +4709,14 @@ "sha.js": "^2.4.8" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4761,6 +4848,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -4797,17 +4885,6 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, - "node_modules/deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dependencies": { - "type-detect": "0.1.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4969,14 +5046,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "engines": { - "node": ">=10" - } - }, "node_modules/drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -6447,6 +6516,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, "node_modules/fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -6693,11 +6767,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-prototype-of": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz", - "integrity": "sha1-mHcr0QcW0W3rSzIlFsRp78oorEQ=" - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7314,16 +7383,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-capitalized": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz", - "integrity": "sha1-TIRktNkdPk7rRIid0s2PGwrEwTY=" - }, - "node_modules/is-class": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/is-class/-/is-class-0.0.4.tgz", - "integrity": "sha1-4FdFFwW7NOOePjNZjJOpg3KWtzY=" - }, "node_modules/is-core-module": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz", @@ -9718,14 +9777,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/keypather": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/keypather/-/keypather-1.10.2.tgz", - "integrity": "sha1-4ESWMtSz5RbyHMAUznxWRP3c5hQ=", - "dependencies": { - "101": "^1.0.0" - } - }, "node_modules/keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -10136,7 +10187,8 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "node_modules/multibase": { "version": "0.6.1", @@ -10272,16 +10324,6 @@ "tweetnacl": "^1.0.1" } }, - "node_modules/near-api-js/node_modules/borsh": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", - "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", - "dependencies": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "text-encoding-utf-8": "^1.0.2" - } - }, "node_modules/near-api-js/node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10325,11 +10367,41 @@ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/node-gyp-build": { @@ -10641,6 +10713,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, "node_modules/parse-asn1": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", @@ -11242,16 +11319,14 @@ } }, "node_modules/rpc-websockets": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.12.tgz", - "integrity": "sha512-WxZRM4443SiYbJhsLwVJc6P/VAQJIkeDS89CQAuHqyMt/GX8GEplWZezcLw6MMGemzA6Kp32kz7CbQppMTLQxA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz", + "integrity": "sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==", "dependencies": { - "@babel/runtime": "^7.11.2", - "assert-args": "^1.2.1", - "circular-json": "^0.5.9", + "@babel/runtime": "^7.17.2", "eventemitter3": "^4.0.7", - "uuid": "^8.3.0", - "ws": "^7.4.5" + "uuid": "^8.3.2", + "ws": "^8.5.0" }, "funding": { "type": "paypal", @@ -11270,6 +11345,26 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/rxjs": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", @@ -12300,6 +12395,11 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -12508,14 +12608,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "engines": { - "node": "*" - } - }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -13650,16 +13742,6 @@ } }, "dependencies": { - "101": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/101/-/101-1.6.3.tgz", - "integrity": "sha512-4dmQ45yY0Dx24Qxp+zAsNLlMF6tteCyfVzgbulvSyC7tCyd3V8sW76sS0tHq8NpcbXfWTKasfyfzU1Kd86oKzw==", - "requires": { - "clone": "^1.0.2", - "deep-eql": "^0.1.3", - "keypather": "^1.10.2" - } - }, "@apollo/client": { "version": "3.6.9", "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", @@ -15430,17 +15512,65 @@ } } }, + "@noble/ed25519": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", + "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==" + }, "@noble/hashes": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz", "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==" }, + "@noble/secp256k1": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz", + "integrity": "sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==" + }, "@openzeppelin/contracts": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.2.0.tgz", "integrity": "sha512-LD4NnkKpHHSMo5z9MvFsG4g1xxZUDqV3A3Futu3nvyfs4wPwXxqOgMaxOoa2PeyGL2VNeSlbxT54enbQzGcgJQ==", "dev": true }, + "@project-serum/anchor": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.25.0.tgz", + "integrity": "sha512-E6A5Y/ijqpfMJ5psJvbw0kVTzLZFUcOFgs6eSM2M2iWE1lVRF18T6hWZVNl6zqZsoz98jgnNHtVGJMs+ds9A7A==", + "requires": { + "@project-serum/borsh": "^0.2.5", + "@solana/web3.js": "^1.36.0", + "base64-js": "^1.5.1", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^5.3.1", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "js-sha256": "^0.9.0", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "dependencies": { + "superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" + } + } + }, + "@project-serum/borsh": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.5.tgz", + "integrity": "sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==", + "requires": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -15542,9 +15672,9 @@ } }, "@solana/buffer-layout": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz", - "integrity": "sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz", + "integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==", "requires": { "buffer": "~6.0.3" }, @@ -15560,17 +15690,25 @@ } } }, - "@solana/spl-token": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz", - "integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==", + "@solana/buffer-layout-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", + "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", "requires": { - "@babel/runtime": "^7.10.5", - "@solana/web3.js": "^1.21.0", - "bn.js": "^5.1.0", - "buffer": "6.0.3", - "buffer-layout": "^1.2.0", - "dotenv": "10.0.0" + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + } + }, + "@solana/spl-token": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.5.tgz", + "integrity": "sha512-0bGC6n415lGjKu02gkLOIpP1wzndSP0SHwN9PefJ+wKAhmfU1rl3AV1Pa41uap2kzSCD6F9642ngNO8KXPvh/g==", + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "buffer": "^6.0.3" }, "dependencies": { "buffer": { @@ -15585,24 +15723,25 @@ } }, "@solana/web3.js": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.24.0.tgz", - "integrity": "sha512-Br3r2YMoM6Ia7NlWVpe+w/cFlRMfW1yXCxy19rxjKZbxIb1i/iEGSOPGsEGCD6FgHJgyWGzD2tf4P1tWra5Fxg==", + "version": "1.66.2", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.66.2.tgz", + "integrity": "sha512-RyaHMR2jGmaesnYP045VLeBGfR/gAW3cvZHzMFGg7bkO+WOYOYp1nEllf0/la4U4qsYGKCsO9eEevR5fhHiVHg==", "requires": { "@babel/runtime": "^7.12.5", - "@solana/buffer-layout": "^3.0.0", + "@noble/ed25519": "^1.7.0", + "@noble/hashes": "^1.1.2", + "@noble/secp256k1": "^1.6.3", + "@solana/buffer-layout": "^4.0.0", + "bigint-buffer": "^1.1.5", "bn.js": "^5.0.0", - "borsh": "^0.4.0", + "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.1", - "crypto-hash": "^1.2.2", + "fast-stable-stringify": "^1.0.0", "jayson": "^3.4.4", - "js-sha3": "^0.8.0", - "node-fetch": "^2.6.1", - "rpc-websockets": "^7.4.2", - "secp256k1": "^4.0.2", - "superstruct": "^0.14.2", - "tweetnacl": "^1.0.0" + "node-fetch": "2", + "rpc-websockets": "^7.5.0", + "superstruct": "^0.14.2" } }, "@szmarczak/http-timer": { @@ -15736,6 +15875,15 @@ "@types/node": "*" } }, + "@types/elliptic": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", + "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", + "dev": true, + "requires": { + "@types/bn.js": "*" + } + }, "@types/express-serve-static-core": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", @@ -15817,6 +15965,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==" }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -16255,19 +16413,6 @@ } } }, - "assert-args": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/assert-args/-/assert-args-1.2.1.tgz", - "integrity": "sha1-QEEDoUUqMv53iYgR5U5ZCoqTc70=", - "requires": { - "101": "^1.2.0", - "compound-subject": "0.0.1", - "debug": "^2.2.0", - "get-prototype-of": "0.0.0", - "is-capitalized": "^1.0.0", - "is-class": "0.0.4" - } - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -16499,11 +16644,24 @@ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==" }, + "bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "requires": { + "bindings": "^1.3.0" + } + }, "bignumber.js": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" }, + "binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==" + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -16595,12 +16753,11 @@ } }, "borsh": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz", - "integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", "requires": { - "@types/bn.js": "^4.11.5", - "bn.js": "^5.0.0", + "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } @@ -16853,8 +17010,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { "version": "1.0.30001277", @@ -16945,11 +17101,6 @@ "safe-buffer": "^5.0.1" } }, - "circular-json": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", - "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==" - }, "cjs-module-lexer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", @@ -16972,11 +17123,6 @@ "wrap-ansi": "^7.0.0" } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -17041,11 +17187,6 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, - "compound-subject": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/compound-subject/-/compound-subject-0.0.1.tgz", - "integrity": "sha1-JxVUaYoVrmCLHfyv0wt7oeqJLEs=" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -17215,6 +17356,14 @@ "sha.js": "^2.4.8" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -17327,6 +17476,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -17357,14 +17507,6 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "requires": { - "type-detect": "0.1.1" - } - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -17499,11 +17641,6 @@ } } }, - "dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" - }, "drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -18580,6 +18717,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -18761,11 +18903,6 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "get-prototype-of": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz", - "integrity": "sha1-mHcr0QcW0W3rSzIlFsRp78oorEQ=" - }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -19224,16 +19361,6 @@ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true }, - "is-capitalized": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz", - "integrity": "sha1-TIRktNkdPk7rRIid0s2PGwrEwTY=" - }, - "is-class": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/is-class/-/is-class-0.0.4.tgz", - "integrity": "sha1-4FdFFwW7NOOePjNZjJOpg3KWtzY=" - }, "is-core-module": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz", @@ -21015,14 +21142,6 @@ } } }, - "keypather": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/keypather/-/keypather-1.10.2.tgz", - "integrity": "sha1-4ESWMtSz5RbyHMAUznxWRP3c5hQ=", - "requires": { - "101": "^1.0.0" - } - }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -21359,7 +21478,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multibase": { "version": "0.6.1", @@ -21465,16 +21585,6 @@ "tweetnacl": "^1.0.1" }, "dependencies": { - "borsh": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", - "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", - "requires": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "text-encoding-utf-8": "^1.0.2" - } - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -21516,9 +21626,33 @@ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-gyp-build": { "version": "4.2.3", @@ -21761,6 +21895,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, "parse-asn1": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", @@ -22241,24 +22380,28 @@ } }, "rpc-websockets": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.12.tgz", - "integrity": "sha512-WxZRM4443SiYbJhsLwVJc6P/VAQJIkeDS89CQAuHqyMt/GX8GEplWZezcLw6MMGemzA6Kp32kz7CbQppMTLQxA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz", + "integrity": "sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==", "requires": { - "@babel/runtime": "^7.11.2", - "assert-args": "^1.2.1", + "@babel/runtime": "^7.17.2", "bufferutil": "^4.0.1", - "circular-json": "^0.5.9", "eventemitter3": "^4.0.7", "utf-8-validate": "^5.0.2", - "uuid": "^8.3.0", - "ws": "^7.4.5" + "uuid": "^8.3.2", + "ws": "^8.5.0" }, "dependencies": { "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "ws": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", + "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "requires": {} } } }, @@ -23066,6 +23209,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -23212,11 +23360,6 @@ "prelude-ls": "~1.1.2" } }, - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=" - }, "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", diff --git a/sdk/js/package.json b/sdk/js/package.json index 431800644..6764caab3 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -12,7 +12,8 @@ "scripts": { "build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json", "build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi src/abi/Wormhole.abi.json", - "build-deps": "npm run build-abis && npm run build-contracts", + "build-idl": "node scripts/compileAnchorIdls.js", + "build-deps": "npm run build-abis && npm run build-contracts && npm run build-idl", "build-lib": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node scripts/copyEthersTypes.js", "build-all": "npm run build-deps && npm run build-lib", "test": "jest --config jestconfig.json --verbose", @@ -43,9 +44,11 @@ "@injectivelabs/tx-ts": "^1.0.22", "@openzeppelin/contracts": "^4.2.0", "@typechain/ethers-v5": "^7.0.1", + "@types/elliptic": "^6.4.14", "@types/jest": "^27.0.2", "@types/long": "^4.0.1", "@types/node": "^16.6.1", + "@types/node-fetch": "^2.6.2", "@types/react": "^17.0.19", "copy-dir": "^1.3.0", "ethers": "^5.6.8", @@ -61,14 +64,17 @@ "@certusone/wormhole-sdk-proto-web": "^0.0.5", "@certusone/wormhole-sdk-wasm": "^0.0.1", "@injectivelabs/sdk-ts": "^1.0.75", - "@solana/spl-token": "^0.1.8", - "@solana/web3.js": "^1.24.0", + "@project-serum/anchor": "^0.25.0", + "@solana/spl-token": "^0.3.5", + "@solana/web3.js": "^1.66.2", "@terra-money/terra.js": "^3.1.3", "@xpla/xpla.js": "^0.2.1", "algosdk": "^1.15.0", "aptos": "^1.3.16", "axios": "^0.24.0", "bech32": "^2.0.0", + "binary-parser": "^2.2.1", + "elliptic": "^6.5.4", "js-base64": "^3.6.1", "near-api-js": "^1.0.0" } diff --git a/sdk/js/scripts/compileAnchorIdls.js b/sdk/js/scripts/compileAnchorIdls.js new file mode 100644 index 000000000..651fc152e --- /dev/null +++ b/sdk/js/scripts/compileAnchorIdls.js @@ -0,0 +1,42 @@ +const fs = require("fs"); + +const SRC_IDL = __dirname + "/../../../solana/idl"; +const DST_IDL = __dirname + "/../src/anchor-idl"; +const TS = __dirname + "/../src/solana/types"; + +const programs = { + "wormhole.json": "Wormhole", + "token_bridge.json": "TokenBridge", + "nft_bridge.json": "NftBridge", +}; + +function main() { + if (!fs.existsSync(DST_IDL)) { + fs.mkdirSync(DST_IDL); + } + + if (!fs.existsSync(TS)) { + fs.mkdirSync(TS); + } + + for (const basename of fs.readdirSync(SRC_IDL)) { + const idl = DST_IDL + "/" + basename; + fs.copyFileSync(SRC_IDL + "/" + basename, idl); + + const targetTypescript = TS + "/" + snakeToCamel(basename).replace("json", "ts"); + + const programType = programs[basename]; + fs.writeFileSync(targetTypescript, `export type ${programType} = `); + fs.appendFileSync(targetTypescript, fs.readFileSync(idl)); + } +} + +const snakeToCamel = str => + str.toLowerCase().replace(/([-_][a-z])/g, group => + group + .toUpperCase() + .replace('-', '') + .replace('_', '') + ); + +main(); \ No newline at end of file diff --git a/sdk/js/src/algorand/__tests__/unit.ts b/sdk/js/src/algorand/__tests__/unit.ts index 61380bbca..b602e4651 100644 --- a/sdk/js/src/algorand/__tests__/unit.ts +++ b/sdk/js/src/algorand/__tests__/unit.ts @@ -4,7 +4,6 @@ import algosdk, { decodeAddress, getApplicationAddress, } from "algosdk"; -import { setDefaultWasm } from "../../solana/wasm"; import { hexToUint8Array, uint8ArrayToHex } from "../../utils"; import { accountExists, @@ -23,8 +22,6 @@ import { PopulateData, TmplSig } from "../TmplSig"; const CORE_ID = BigInt(4); const TOKEN_BRIDGE_ID = BigInt(6); -setDefaultWasm("node"); - jest.setTimeout(60000); describe("Unit Tests", () => { diff --git a/sdk/js/src/bridge/getClaimAddress.ts b/sdk/js/src/bridge/getClaimAddress.ts index 24cf2a1bf..bfdf97ca9 100644 --- a/sdk/js/src/bridge/getClaimAddress.ts +++ b/sdk/js/src/bridge/getClaimAddress.ts @@ -1,10 +1,16 @@ -import { PublicKey } from "@solana/web3.js"; -import { importCoreWasm } from "../solana/wasm"; +import { PublicKeyInitData } from "@solana/web3.js"; +import { deriveClaimKey } from "../solana/wormhole"; +import { parseVaa, SignedVaa } from "../vaa/wormhole"; export async function getClaimAddressSolana( - programAddress: string, - signedVAA: Uint8Array + programAddress: PublicKeyInitData, + signedVaa: SignedVaa ) { - const { claim_address } = await importCoreWasm(); - return new PublicKey(claim_address(programAddress, signedVAA)); + const parsed = parseVaa(signedVaa); + return deriveClaimKey( + programAddress, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ); } diff --git a/sdk/js/src/bridge/getEmitterAddress.ts b/sdk/js/src/bridge/getEmitterAddress.ts index 034c587f6..520b05b30 100644 --- a/sdk/js/src/bridge/getEmitterAddress.ts +++ b/sdk/js/src/bridge/getEmitterAddress.ts @@ -1,4 +1,4 @@ -import { PublicKey } from "@solana/web3.js"; +import { PublicKeyInitData } from "@solana/web3.js"; import { decodeAddress, getApplicationAddress } from "algosdk"; import { bech32 } from "bech32"; import { @@ -8,7 +8,7 @@ import { Hexable, zeroPad, } from "ethers/lib/utils"; -import { importTokenWasm } from "../solana/wasm"; +import { deriveWormholeEmitterKey } from "../solana/wormhole"; import { uint8ArrayToHex } from "../utils"; export function getEmitterAddressEth( @@ -17,11 +17,10 @@ export function getEmitterAddressEth( return Buffer.from(zeroPad(arrayify(contractAddress), 32)).toString("hex"); } -export async function getEmitterAddressSolana(programAddress: string) { - const { emitter_address } = await importTokenWasm(); - return Buffer.from( - zeroPad(new PublicKey(emitter_address(programAddress)).toBytes(), 32) - ).toString("hex"); +export async function getEmitterAddressSolana( + programAddress: PublicKeyInitData +) { + return deriveWormholeEmitterKey(programAddress).toBuffer().toString("hex"); } export async function getEmitterAddressTerra(programAddress: string) { diff --git a/sdk/js/src/bridge/getSignedVAAHash.ts b/sdk/js/src/bridge/getSignedVAAHash.ts index 10a3ad1e3..1cf460cae 100644 --- a/sdk/js/src/bridge/getSignedVAAHash.ts +++ b/sdk/js/src/bridge/getSignedVAAHash.ts @@ -1,18 +1,6 @@ -import { importCoreWasm } from "../solana/wasm"; -import { ethers } from "ethers"; -import { uint8ArrayToHex } from ".."; +import { keccak256 } from "../utils"; +import { parseVaa, SignedVaa } from "../vaa/wormhole"; -export async function getSignedVAAHash(signedVAA: Uint8Array) { - const { parse_vaa } = await importCoreWasm(); - const parsedVAA = parse_vaa(signedVAA); - const body = [ - ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.timestamp]).substring(2 + (64 - 8)), - ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.nonce]).substring(2 + (64 - 8)), - ethers.utils.defaultAbiCoder.encode(["uint16"], [parsedVAA.emitter_chain]).substring(2 + (64 - 4)), - ethers.utils.defaultAbiCoder.encode(["bytes32"], [parsedVAA.emitter_address]).substring(2), - ethers.utils.defaultAbiCoder.encode(["uint64"], [parsedVAA.sequence]).substring(2 + (64 - 16)), - ethers.utils.defaultAbiCoder.encode(["uint8"], [parsedVAA.consistency_level]).substring(2 + (64 - 2)), - uint8ArrayToHex(parsedVAA.payload), - ]; - return ethers.utils.solidityKeccak256(["bytes"], [ethers.utils.solidityKeccak256(["bytes"], ["0x" + body.join("")])]); +export function getSignedVAAHash(signedVaa: SignedVaa): string { + return `0x${keccak256(parseVaa(signedVaa).hash).toString("hex")}`; } diff --git a/sdk/js/src/cosmwasm/query.testnet.test.ts b/sdk/js/src/cosmwasm/query.testnet.test.ts index 83a87fad1..ad4fe76e2 100644 --- a/sdk/js/src/cosmwasm/query.testnet.test.ts +++ b/sdk/js/src/cosmwasm/query.testnet.test.ts @@ -1,4 +1,3 @@ -require("dotenv").config({ path: ".env" }); import { getNetworkInfo, Network } from "@injectivelabs/networks"; import { ChainGrpcWasmApi, diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index f9b6e2d4e..2f91af12d 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -1,12 +1,9 @@ -export * from "./cosmos"; -export * from "./ethers-contracts"; -export * from "./solana"; -export * from "./terra"; export * from "./rpc"; export * from "./utils"; export * from "./bridge"; export * from "./token_bridge"; export * from "./cosmwasm"; +export * from "./vaa"; export * as cosmos from "./cosmos"; export * as ethers_contracts from "./ethers-contracts"; @@ -18,3 +15,5 @@ export * as bridge from "./bridge"; export * as token_bridge from "./token_bridge"; export * as nft_bridge from "./nft_bridge"; export * as algorand from "./algorand"; + +export { postVaaSolana, postVaaSolanaWithRetry } from "./solana"; diff --git a/sdk/js/src/migration/addLiquidity.ts b/sdk/js/src/migration/addLiquidity.ts deleted file mode 100644 index 0f017d9af..000000000 --- a/sdk/js/src/migration/addLiquidity.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { ixFromRust } from "../solana"; -import { importMigrationWasm } from "../solana/wasm"; - -export default async function addLiquidity( - connection: Connection, - payerAddress: string, - program_id: string, - from_mint: string, - to_mint: string, - liquidity_token_account: string, - lp_share_token_account: string, - amount: BigInt -) { - const { authority_address, add_liquidity } = await importMigrationWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(liquidity_token_account), - new PublicKey(authority_address(program_id)), - new PublicKey(payerAddress), - [], - new u64(amount.toString(16), 16) - ); - const ix = ixFromRust( - add_liquidity( - program_id, - from_mint, - to_mint, - liquidity_token_account, - lp_share_token_account, - amount.valueOf() - ) - ); - const transaction = new Transaction().add(approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - return transaction; -} diff --git a/sdk/js/src/migration/authorityAddress.ts b/sdk/js/src/migration/authorityAddress.ts deleted file mode 100644 index ba657172e..000000000 --- a/sdk/js/src/migration/authorityAddress.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function authorityAddress(program_id: string) { - const { authority_address } = await importMigrationWasm(); - return authority_address(program_id); -} diff --git a/sdk/js/src/migration/claimShares.ts b/sdk/js/src/migration/claimShares.ts deleted file mode 100644 index 7470e9151..000000000 --- a/sdk/js/src/migration/claimShares.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { ixFromRust } from "../solana"; -import { importMigrationWasm } from "../solana/wasm"; - -export default async function claimShares( - connection: Connection, - payerAddress: string, - program_id: string, - from_mint: string, - to_mint: string, - output_token_account: string, - lp_share_token_account: string, - amount: BigInt -) { - const { authority_address, claim_shares } = await importMigrationWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(lp_share_token_account), - new PublicKey(authority_address(program_id)), - new PublicKey(payerAddress), - [], - new u64(amount.toString(16), 16) - ); - const ix = ixFromRust( - claim_shares( - program_id, - from_mint, - to_mint, - output_token_account, - lp_share_token_account, - amount.valueOf() - ) - ); - const transaction = new Transaction().add(approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - return transaction; -} diff --git a/sdk/js/src/migration/createPool.ts b/sdk/js/src/migration/createPool.ts deleted file mode 100644 index 7235e95f6..000000000 --- a/sdk/js/src/migration/createPool.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { ixFromRust } from "../solana"; -import { importMigrationWasm } from "../solana/wasm"; - -export default async function createPool( - connection: Connection, - payerAddress: string, - program_id: string, - payer: string, - from_mint: string, - to_mint: string -) { - const { create_pool } = await importMigrationWasm(); - const ix = ixFromRust(create_pool(program_id, payer, from_mint, to_mint)); - const transaction = new Transaction().add(ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - return transaction; -} diff --git a/sdk/js/src/migration/fromCustodyAddress.ts b/sdk/js/src/migration/fromCustodyAddress.ts deleted file mode 100644 index 43b4652da..000000000 --- a/sdk/js/src/migration/fromCustodyAddress.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function fromCustodyAddress( - program_id: string, - pool: string -) { - const { from_custody_address } = await importMigrationWasm(); - return from_custody_address(program_id, pool); -} diff --git a/sdk/js/src/migration/migrateTokens.ts b/sdk/js/src/migration/migrateTokens.ts deleted file mode 100644 index 7a4ea7254..000000000 --- a/sdk/js/src/migration/migrateTokens.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { ixFromRust } from "../solana"; -import { importMigrationWasm } from "../solana/wasm"; - -export default async function migrateTokens( - connection: Connection, - payerAddress: string, - program_id: string, - from_mint: string, - to_mint: string, - input_token_account: string, - output_token_account: string, - amount: BigInt -) { - const { authority_address, migrate_tokens } = await importMigrationWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(input_token_account), - new PublicKey(authority_address(program_id)), - new PublicKey(payerAddress), - [], - new u64(amount.toString(16), 16) - ); - const ix = ixFromRust( - migrate_tokens( - program_id, - from_mint, - to_mint, - input_token_account, - output_token_account, - amount.valueOf() - ) - ); - const transaction = new Transaction().add(approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - return transaction; -} diff --git a/sdk/js/src/migration/parsePool.ts b/sdk/js/src/migration/parsePool.ts deleted file mode 100644 index fcfb37717..000000000 --- a/sdk/js/src/migration/parsePool.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function parsePool(data: Uint8Array) { - const { parse_pool } = await importMigrationWasm(); - return parse_pool(data); -} diff --git a/sdk/js/src/migration/poolAddress.ts b/sdk/js/src/migration/poolAddress.ts deleted file mode 100644 index 0d1aaf7d3..000000000 --- a/sdk/js/src/migration/poolAddress.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function poolAddress( - program_id: string, - from_mint: string, - to_mint: string -) { - const { pool_address } = await importMigrationWasm(); - return pool_address(program_id, from_mint, to_mint); -} diff --git a/sdk/js/src/migration/removeLiquidity.ts b/sdk/js/src/migration/removeLiquidity.ts deleted file mode 100644 index ef3292ae3..000000000 --- a/sdk/js/src/migration/removeLiquidity.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { ixFromRust } from "../solana"; -import { importMigrationWasm } from "../solana/wasm"; - -export default async function removeLiquidity( - connection: Connection, - payerAddress: string, - program_id: string, - from_mint: string, - to_mint: string, - liquidity_token_account: string, - lp_share_token_account: string, - amount: BigInt -) { - const { authority_address, remove_liquidity } = await importMigrationWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(lp_share_token_account), - new PublicKey(authority_address(program_id)), - new PublicKey(payerAddress), - [], - new u64(amount.toString(16), 16) - ); - const ix = ixFromRust( - remove_liquidity( - program_id, - from_mint, - to_mint, - liquidity_token_account, - lp_share_token_account, - amount.valueOf() - ) - ); - const transaction = new Transaction().add(approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - return transaction; -} diff --git a/sdk/js/src/migration/shareMintAddress.ts b/sdk/js/src/migration/shareMintAddress.ts deleted file mode 100644 index eda0d1f8a..000000000 --- a/sdk/js/src/migration/shareMintAddress.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function shareMintAddress( - program_id: string, - pool: string -) { - const { share_mint_address } = await importMigrationWasm(); - return share_mint_address(program_id, pool); -} diff --git a/sdk/js/src/migration/toCustodyAddress.ts b/sdk/js/src/migration/toCustodyAddress.ts deleted file mode 100644 index 32be18a6b..000000000 --- a/sdk/js/src/migration/toCustodyAddress.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { importMigrationWasm } from "../solana/wasm"; - -export default async function toCustodyAddress( - program_id: string, - pool: string -) { - const { to_custody_address } = await importMigrationWasm(); - return to_custody_address(program_id, pool); -} diff --git a/sdk/js/src/mock/governance.ts b/sdk/js/src/mock/governance.ts new file mode 100644 index 000000000..c0c9eee87 --- /dev/null +++ b/sdk/js/src/mock/governance.ts @@ -0,0 +1,205 @@ +import { BN } from "@project-serum/anchor"; +import { ChainId, tryNativeToHexString } from "../utils"; +import { MockEmitter } from "./wormhole"; + +const ETHEREUM_KEY_LENGTH = 20; + +export class GovernanceEmitter extends MockEmitter { + constructor(emitterAddress: string, startSequence?: number) { + super(emitterAddress, 1, startSequence); + } + + publishGovernanceMessage( + timestamp: number, + module: string, + payload: Buffer, + action: number, + chain: number, + uptickSequence: boolean = true + ) { + const serialized = Buffer.alloc(35 + payload.length); + + const moduleBytes = Buffer.alloc(32); + moduleBytes.write(module, 32 - module.length); + serialized.write(moduleBytes.toString("hex"), 0, "hex"); + serialized.writeUInt8(action, 32); // action + serialized.writeUInt16BE(chain, 33); + serialized.write(payload.toString("hex"), 35, "hex"); + return this.publishMessage(0, serialized, 1, timestamp, uptickSequence); + } + + publishWormholeSetMessageFee( + timestamp: number, + chain: number, + amount: bigint, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(32); + const amountBytes = new BN(amount.toString()).toBuffer(); + payload.write(amountBytes.toString("hex"), 32 - amountBytes.length, "hex"); + return this.publishGovernanceMessage( + timestamp, + "Core", + payload, + 3, + chain, + uptickSequence + ); + } + + publishWormholeTransferFees( + timestamp: number, + chain: number, + amount: bigint, + recipient: Buffer, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(64); + const amountBytes = new BN(amount.toString()).toBuffer(); + payload.write(amountBytes.toString("hex"), 32 - amountBytes.length, "hex"); + payload.write(recipient.toString("hex"), 32, "hex"); + return this.publishGovernanceMessage( + timestamp, + "Core", + payload, + 4, + chain, + uptickSequence + ); + } + + publishWormholeGuardianSetUpgrade( + timestamp: number, + newGuardianSetIndex: number, + publicKeys: Buffer[], + uptickSequence: boolean = true + ) { + const numKeys = publicKeys.length; + const payload = Buffer.alloc(5 + ETHEREUM_KEY_LENGTH * numKeys); + payload.writeUInt32BE(newGuardianSetIndex, 0); + payload.writeUInt8(numKeys, 4); + for (let i = 0; i < numKeys; ++i) { + const publicKey = publicKeys.at(i); + if (publicKey == undefined) { + throw Error("publicKey == undefined"); + } + payload.write( + publicKey.toString("hex"), + 5 + ETHEREUM_KEY_LENGTH * i, + "hex" + ); + } + return this.publishGovernanceMessage( + timestamp, + "Core", + payload, + 2, + 0, + uptickSequence + ); + } + + publishWormholeUpgradeContract( + timestamp: number, + chain: number, + newContract: string, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(32); + payload.write( + tryNativeToHexString(newContract, chain as ChainId), + 0, + "hex" + ); + return this.publishGovernanceMessage( + timestamp, + "Core", + payload, + 1, + chain, + uptickSequence + ); + } + + publishTokenBridgeRegisterChain( + timestamp: number, + chain: number, + address: string, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(34); + payload.writeUInt16BE(chain, 0); + payload.write(tryNativeToHexString(address, chain as ChainId), 2, "hex"); + return this.publishGovernanceMessage( + timestamp, + "TokenBridge", + payload, + 1, + 0, + uptickSequence + ); + } + + publishTokenBridgeUpgradeContract( + timestamp: number, + chain: number, + newContract: string, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(32); + payload.write( + tryNativeToHexString(newContract, this.chain as ChainId), + 0, + "hex" + ); + return this.publishGovernanceMessage( + timestamp, + "TokenBridge", + payload, + 2, + chain, + uptickSequence + ); + } + + publishNftBridgeRegisterChain( + timestamp: number, + chain: number, + address: string, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(34); + payload.writeUInt16BE(chain, 0); + payload.write(tryNativeToHexString(address, chain as ChainId), 2, "hex"); + return this.publishGovernanceMessage( + timestamp, + "NFTBridge", + payload, + 1, + 0, + uptickSequence + ); + } + + publishNftBridgeUpgradeContract( + timestamp: number, + chain: number, + newContract: string, + uptickSequence: boolean = true + ) { + const payload = Buffer.alloc(32); + payload.write( + tryNativeToHexString(newContract, this.chain as ChainId), + 0, + "hex" + ); + return this.publishGovernanceMessage( + timestamp, + "NFTBridge", + payload, + 2, + chain, + uptickSequence + ); + } +} diff --git a/sdk/js/src/mock/index.ts b/sdk/js/src/mock/index.ts new file mode 100644 index 000000000..e808b826b --- /dev/null +++ b/sdk/js/src/mock/index.ts @@ -0,0 +1,5 @@ +export * from "./governance"; +export * from "./misc"; +export * from "./nftBridge"; +export * from "./tokenBridge"; +export * from "./wormhole"; diff --git a/sdk/js/src/mock/misc.ts b/sdk/js/src/mock/misc.ts new file mode 100644 index 000000000..0ded8ee8f --- /dev/null +++ b/sdk/js/src/mock/misc.ts @@ -0,0 +1,17 @@ +import { keccak256 } from "../utils"; +import * as elliptic from "elliptic"; + +export function ethPrivateToPublic(key: string) { + const ecdsa = new elliptic.ec("secp256k1"); + const publicKey = ecdsa.keyFromPrivate(key).getPublic("hex"); + return keccak256(Buffer.from(publicKey, "hex").subarray(1)).subarray(12); +} + +export function ethSignWithPrivate(privateKey: string, hash: Buffer) { + if (hash.length != 32) { + throw new Error("hash.length != 32"); + } + const ecdsa = new elliptic.ec("secp256k1"); + const key = ecdsa.keyFromPrivate(privateKey); + return key.sign(hash, { canonical: true }); +} diff --git a/sdk/js/src/mock/nftBridge.ts b/sdk/js/src/mock/nftBridge.ts new file mode 100644 index 000000000..177e20fbf --- /dev/null +++ b/sdk/js/src/mock/nftBridge.ts @@ -0,0 +1,87 @@ +import { NodePrivilegedServiceChainGovernorReleasePendingVAADesc } from "@certusone/wormhole-sdk-proto-web/lib/cjs/node/v1/node"; +import { BN } from "@project-serum/anchor"; +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { ChainId, tryNativeToHexString } from "../utils"; +import { MockEmitter } from "./wormhole"; + +export class MockNftBridge extends MockEmitter { + consistencyLevel: number; + + constructor(emitterAddress: string, chain: number, consistencyLevel: number) { + super(emitterAddress, chain); + this.consistencyLevel = consistencyLevel; + } + + publishNftBridgeMessage( + serialized: Buffer, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return this.publishMessage( + nonce == undefined ? 0 : nonce, + serialized, + this.consistencyLevel, + timestamp, + uptickSequence + ); + } + + publishTransferNft( + tokenAddress: string, + tokenChain: number, + name: string, + symbol: string, + tokenId: bigint, + uri: string, + recipientChain: number, + recipient: string, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + if (uri.length > 200) { + throw new Error("uri.length > 200"); + } + const serialized = Buffer.alloc(166 + uri.length); + serialized.writeUInt8(1, 0); + serialized.write(tokenAddress, 1, "hex"); + serialized.writeUInt16BE(tokenChain, 33); + // truncate to 32 characters + symbol = symbol.substring(0, 32); + serialized.write(symbol, 35); + // truncate to 32 characters + name = name.substring(0, 32); + serialized.write(name, 67); + const tokenIdBytes = new BN(tokenId.toString()).toBuffer(); + serialized.write( + tokenIdBytes.toString("hex"), + 131 - tokenIdBytes.length, + "hex" + ); + serialized.writeUInt8(uri.length, 131); + serialized.write(uri, 132); + const uriEnd = 132 + uri.length; + serialized.write(recipient, uriEnd, "hex"); + serialized.writeUInt16BE(recipientChain, uriEnd + 32); + return this.publishNftBridgeMessage( + serialized, + nonce, + timestamp, + uptickSequence + ); + } +} + +export class MockEthereumNftBridge extends MockNftBridge { + constructor(emitterAddress: string) { + const chain = 2; + super(tryNativeToHexString(emitterAddress, chain as ChainId), chain, 15); + } +} + +export class MockSolanaNftBridge extends MockNftBridge { + constructor(emitterAddress: PublicKeyInitData) { + super(new PublicKey(emitterAddress).toBuffer().toString("hex"), 1, 32); + } +} diff --git a/sdk/js/src/mock/tokenBridge.ts b/sdk/js/src/mock/tokenBridge.ts new file mode 100644 index 000000000..3a335aad1 --- /dev/null +++ b/sdk/js/src/mock/tokenBridge.ts @@ -0,0 +1,209 @@ +import { BN } from "@project-serum/anchor"; +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { ChainId, tryNativeToHexString } from "../utils"; +import { MockEmitter } from "./wormhole"; + +export class MockTokenBridge extends MockEmitter { + consistencyLevel: number; + + constructor(emitterAddress: string, chain: number, consistencyLevel: number) { + super(emitterAddress, chain); + this.consistencyLevel = consistencyLevel; + } + + publishTokenBridgeMessage( + serialized: Buffer, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return this.publishMessage( + nonce == undefined ? 0 : nonce, + serialized, + this.consistencyLevel, + timestamp, + uptickSequence + ); + } + + publishAttestMeta( + tokenAddress: string, + decimals: number, + symbol: string, + name: string, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + const serialized = Buffer.alloc(100); + serialized.writeUInt8(2, 0); + const hexlified = Buffer.from(tokenAddress, "hex"); + if (hexlified.length != 32) { + throw new Error("tokenAddress must be 32 bytes"); + } + serialized.write(hexlified.toString("hex"), 1, "hex"); + serialized.writeUInt16BE(this.chain, 33); + serialized.writeUInt8(decimals, 35); + // truncate to 32 characters + symbol = symbol.substring(0, 32); + serialized.write(symbol, 68 - symbol.length); + // truncate to 32 characters + name = name.substring(0, 32); + serialized.write(name, 100 - name.length); + return this.publishTokenBridgeMessage( + serialized, + nonce, + timestamp, + uptickSequence + ); + } + + serializeTransferOnly( + withPayload: boolean, + tokenAddress: string, + tokenChain: number, + amount: bigint, + recipientChain: number, + recipient: string, + fee?: bigint, + fromAddress?: Buffer + ) { + const serialized = Buffer.alloc(133); + serialized.writeUInt8(1, 0); + const amountBytes = new BN(amount.toString()).toBuffer(); + serialized.write( + amountBytes.toString("hex"), + 33 - amountBytes.length, + "hex" + ); + serialized.write(tokenAddress, 33, "hex"); + serialized.writeUInt16BE(tokenChain, 65); + serialized.write(recipient, 67, "hex"); + serialized.writeUInt16BE(recipientChain, 99); + if (withPayload) { + if (fromAddress === undefined) { + throw new Error("fromAddress === undefined"); + } + serialized.write(fromAddress.toString("hex"), 101, "hex"); + } else { + if (fee === undefined) { + throw new Error("fee === undefined"); + } + const feeBytes = new BN(fee.toString()).toBuffer(); + serialized.write(feeBytes.toString("hex"), 133 - feeBytes.length, "hex"); + } + return serialized; + } + + publishTransferTokens( + tokenAddress: string, + tokenChain: number, + amount: bigint, + recipientChain: number, + recipient: string, + fee: bigint, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return this.publishTokenBridgeMessage( + this.serializeTransferOnly( + false, // withPayload + tokenAddress, + tokenChain, + amount, + recipientChain, + recipient, + fee + ), + nonce, + timestamp, + uptickSequence + ); + } + + publishTransferTokensWithPayload( + tokenAddress: string, + tokenChain: number, + amount: bigint, + recipientChain: number, + recipient: string, + fromAddress: Buffer, + payload: Buffer, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return this.publishTokenBridgeMessage( + Buffer.concat([ + this.serializeTransferOnly( + true, // withPayload + tokenAddress, + tokenChain, + amount, + recipientChain, + recipient, + undefined, // fee + fromAddress + ), + payload, + ]), + nonce, + timestamp, + uptickSequence + ); + } +} + +export class MockEthereumTokenBridge extends MockTokenBridge { + constructor(emitterAddress: string) { + const chain = 2; + super(tryNativeToHexString(emitterAddress, chain as ChainId), chain, 15); + } + + publishAttestMeta( + tokenAddress: string, + decimals: number, + symbol: string, + name: string, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return super.publishAttestMeta( + tryNativeToHexString(tokenAddress, this.chain as ChainId), + decimals, + symbol == undefined ? "" : symbol, + name == undefined ? "" : name, + nonce, + timestamp, + uptickSequence + ); + } +} + +export class MockSolanaTokenBridge extends MockTokenBridge { + constructor(emitterAddress: PublicKeyInitData) { + super(new PublicKey(emitterAddress).toBuffer().toString("hex"), 1, 32); + } + + publishAttestMeta( + mint: PublicKeyInitData, + decimals: number, + symbol?: string, + name?: string, + nonce?: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + return super.publishAttestMeta( + new PublicKey(mint).toBuffer().toString("hex"), + decimals, + symbol == undefined ? "" : symbol, + name == undefined ? "" : name, + nonce, + timestamp, + uptickSequence + ); + } +} diff --git a/sdk/js/src/mock/wormhole.ts b/sdk/js/src/mock/wormhole.ts new file mode 100644 index 000000000..42e858445 --- /dev/null +++ b/sdk/js/src/mock/wormhole.ts @@ -0,0 +1,129 @@ +import { keccak256 } from "../utils"; +import { ethPrivateToPublic, ethSignWithPrivate } from "./misc"; + +const SIGNATURE_PAYLOAD_LEN = 66; + +interface Guardian { + index: number; + key: string; +} + +export class MockGuardians { + setIndex: number; + signers: Guardian[]; + + constructor(setIndex: number, keys: string[]) { + this.setIndex = setIndex; + this.signers = keys.map((key, index): Guardian => { + return { index, key }; + }); + } + + getPublicKeys() { + return this.signers.map((guardian) => ethPrivateToPublic(guardian.key)); + } + + updateGuardianSetIndex(setIndex: number) { + this.setIndex = setIndex; + } + + addSignatures(message: Buffer, guardianIndices: number[]) { + if (guardianIndices.length == 0) { + throw Error("guardianIndices.length == 0"); + } + const signers = this.signers.filter((signer) => + guardianIndices.includes(signer.index) + ); + + const sigStart = 6; + const numSigners = signers.length; + + const signedVaa = Buffer.alloc( + sigStart + SIGNATURE_PAYLOAD_LEN * numSigners + message.length + ); + signedVaa.write( + message.toString("hex"), + sigStart + SIGNATURE_PAYLOAD_LEN * numSigners, + "hex" + ); + + signedVaa.writeUInt8(1, 0); + signedVaa.writeUInt32BE(this.setIndex, 1); + signedVaa.writeUInt8(numSigners, 5); + + // signatures + const hash = keccak256(keccak256(message)); + + for (let i = 0; i < numSigners; ++i) { + const signer = signers.at(i); + if (signer == undefined) { + throw Error("signer == undefined"); + } + const signature = ethSignWithPrivate(signer.key, hash); + + const start = sigStart + i * SIGNATURE_PAYLOAD_LEN; + signedVaa.writeUInt8(signer.index, start); + signedVaa.write( + signature.r.toString(16).padStart(64, "0"), + start + 1, + "hex" + ); + signedVaa.write( + signature.s.toString(16).padStart(64, "0"), + start + 33, + "hex" + ); + signedVaa.writeUInt8(signature.recoveryParam!, start + 65); + } + + return signedVaa; + } +} + +export class MockEmitter { + chain: number; + address: Buffer; + + sequence: number; + + constructor(emitterAddress: string, chain: number, startSequence?: number) { + this.chain = chain; + const address = Buffer.from(emitterAddress, "hex"); + if (address.length != 32) { + throw Error("emitterAddress.length != 32"); + } + this.address = address; + + this.sequence = startSequence == undefined ? 0 : startSequence; + } + + publishMessage( + nonce: number, + payload: Buffer, + consistencyLevel: number, + timestamp?: number, + uptickSequence: boolean = true + ) { + if (uptickSequence) { + ++this.sequence; + } + + const message = Buffer.alloc(51 + payload.length); + + message.writeUInt32BE(timestamp == undefined ? 0 : timestamp, 0); + message.writeUInt32BE(nonce, 4); + message.writeUInt16BE(this.chain, 8); + message.write(this.address.toString("hex"), 10, "hex"); + message.writeBigUInt64BE(BigInt(this.sequence), 42); + message.writeUInt8(consistencyLevel, 50); + message.write(payload.toString("hex"), 51, "hex"); + + return message; + } +} + +export class MockEthereumEmitter extends MockEmitter { + constructor(emitterAddress: string, chain?: number) { + super(emitterAddress, chain == undefined ? 2 : chain); + } +} diff --git a/sdk/js/src/nft_bridge/__tests__/integration.ts b/sdk/js/src/nft_bridge/__tests__/integration.ts index f3687c2af..8481d9430 100644 --- a/sdk/js/src/nft_bridge/__tests__/integration.ts +++ b/sdk/js/src/nft_bridge/__tests__/integration.ts @@ -11,7 +11,7 @@ import { } from "@jest/globals"; import { ASSOCIATED_TOKEN_PROGRAM_ID, - Token, + getAssociatedTokenAddress, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { BigNumber, BigNumberish, ethers } from "ethers"; @@ -28,10 +28,8 @@ import { parseSequenceFromLogSolana, getEmitterAddressSolana, CHAIN_ID_SOLANA, - parseNFTPayload, } from "../.."; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { importCoreWasm, setDefaultWasm } from "../../solana/wasm"; import { ETH_NODE_URL, ETH_PRIVATE_KEY, @@ -59,10 +57,10 @@ import { import { postVaaSolanaWithRetry } from "../../solana"; import { tryNativeToUint8Array } from "../../utils"; import { arrayify } from "ethers/lib/utils"; +import { parseVaa } from "../../vaa/wormhole"; +import { parseNftTransferVaa } from "../../vaa"; const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json"); -setDefaultWasm("node"); - jest.setTimeout(60000); type Address = string; @@ -158,11 +156,7 @@ describe("Integration Tests", () => { test("Send Solana SPL to Ethereum and back", (done) => { (async () => { try { - const { parse_vaa } = await importCoreWasm(); - - const fromAddress = await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, + const fromAddress = await getAssociatedTokenAddress( new PublicKey(TEST_SOLANA_TOKEN), keypair.publicKey ); @@ -177,9 +171,7 @@ describe("Integration Tests", () => { let signedVAA = await waitUntilSolanaTxObserved(transaction1); // we get the solana token id from the VAA - const { tokenId } = parseNFTPayload( - Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload)) - ); + const { tokenId } = parseNftTransferVaa(signedVAA); await _redeemOnEth(signedVAA); const eth_addr = await nft_bridge.getForeignAssetEth( @@ -200,9 +192,7 @@ describe("Integration Tests", () => { ); signedVAA = await waitUntilEthTxObserved(transaction3); - const { name, symbol } = parseNFTPayload( - Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload)) - ); + const { name, symbol } = parseNftTransferVaa(signedVAA); // if the names match up here, it means all the spl caches work expect(name).toBe("Not a PUNK🎸"); diff --git a/sdk/js/src/nft_bridge/getForeignAsset.ts b/sdk/js/src/nft_bridge/getForeignAsset.ts index a1528e71e..f47941325 100644 --- a/sdk/js/src/nft_bridge/getForeignAsset.ts +++ b/sdk/js/src/nft_bridge/getForeignAsset.ts @@ -1,28 +1,30 @@ -import { PublicKey } from "@solana/web3.js"; +import { BN } from "@project-serum/anchor"; +import { PublicKeyInitData } from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import { ethers } from "ethers"; +import { isBytes } from "ethers/lib/utils"; import { fromUint8Array } from "js-base64"; import { CHAIN_ID_SOLANA } from ".."; import { NFTBridge__factory } from "../ethers-contracts"; -import { importNftWasm } from "../solana/wasm"; +import { deriveWrappedMintKey } from "../solana/nftBridge"; import { ChainId, ChainName, coalesceChainId } from "../utils"; /** * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param provider * @param originChain * @param originAsset zero pad to 32 bytes * @returns */ export async function getForeignAssetEth( - tokenBridgeAddress: string, + nftBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, originChain: ChainId | ChainName, originAsset: Uint8Array ): Promise { const originChainId = coalesceChainId(originChain); - const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider); + const tokenBridge = NFTBridge__factory.connect(nftBridgeAddress, provider); try { if (originChainId === CHAIN_ID_SOLANA) { // All NFTs from Solana are minted to the same address, the originAsset is encoded as the tokenId as @@ -41,14 +43,14 @@ export async function getForeignAssetEth( /** * Returns a foreign asset address on Terra for a provided native chain and asset address - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param client * @param originChain * @param originAsset * @returns */ export async function getForeignAssetTerra( - tokenBridgeAddress: string, + nftBridgeAddress: string, client: LCDClient, originChain: ChainId, originAsset: Uint8Array @@ -60,7 +62,7 @@ export async function getForeignAssetTerra( ? "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=" : fromUint8Array(originAsset); const result: { address: string } = await client.wasm.contractQuery( - tokenBridgeAddress, + nftBridgeAddress, { wrapped_registry: { chain: originChainId, @@ -76,26 +78,24 @@ export async function getForeignAssetTerra( /** * Returns a foreign asset address on Solana for a provided native chain and asset address - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param originChain * @param originAsset zero pad to 32 bytes * @returns */ -export async function getForeignAssetSol( - tokenBridgeAddress: string, +export async function getForeignAssetSolana( + nftBridgeAddress: PublicKeyInitData, originChain: ChainId | ChainName, - originAsset: Uint8Array, - tokenId: Uint8Array + originAsset: string | Uint8Array | Buffer, + tokenId: Uint8Array | Buffer | bigint ): Promise { - const originChainId = coalesceChainId(originChain); - const { wrapped_address } = await importNftWasm(); - const wrappedAddress = wrapped_address( - tokenBridgeAddress, - originAsset, - originChainId, - tokenId - ); - const wrappedAddressPK = new PublicKey(wrappedAddress); // we don't require NFT accounts to exist, so don't check them. - return wrappedAddressPK.toString(); + return deriveWrappedMintKey( + nftBridgeAddress, + coalesceChainId(originChain) as number, + originAsset, + isBytes(tokenId) ? BigInt(new BN(tokenId).toString()) : tokenId + ).toString(); } + +export const getForeignAssetSol = getForeignAssetSolana; diff --git a/sdk/js/src/nft_bridge/getIsTransferCompleted.ts b/sdk/js/src/nft_bridge/getIsTransferCompleted.ts index d85c34822..7c7c3e603 100644 --- a/sdk/js/src/nft_bridge/getIsTransferCompleted.ts +++ b/sdk/js/src/nft_bridge/getIsTransferCompleted.ts @@ -1,12 +1,13 @@ import { ethers } from "ethers"; import { NFTBridge__factory } from "../ethers-contracts"; import { getSignedVAAHash } from "../bridge"; -import { importCoreWasm } from "../solana/wasm"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import axios from "axios"; import { redeemOnTerra } from "."; import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from ".."; +import { getClaim } from "../solana/wormhole"; +import { parseVaa, SignedVaa } from "../vaa/wormhole"; export async function getIsTransferCompletedEth( nftBridgeAddress: string, @@ -14,7 +15,7 @@ export async function getIsTransferCompletedEth( signedVAA: Uint8Array ) { const nftBridge = NFTBridge__factory.connect(nftBridgeAddress, provider); - const signedVAAHash = await getSignedVAAHash(signedVAA); + const signedVAAHash = getSignedVAAHash(signedVAA); return await nftBridge.isTransferCompleted(signedVAAHash); } @@ -57,15 +58,18 @@ export async function getIsTransferCompletedTerra( } export async function getIsTransferCompletedSolana( - nftBridgeAddress: string, - signedVAA: Uint8Array, - connection: Connection + nftBridgeAddress: PublicKeyInitData, + signedVAA: SignedVaa, + connection: Connection, + commitment?: Commitment ) { - const { claim_address } = await importCoreWasm(); - const claimAddress = await claim_address(nftBridgeAddress, signedVAA); - const claimInfo = await connection.getAccountInfo( - new PublicKey(claimAddress), - "confirmed" - ); - return !!claimInfo; + const parsed = parseVaa(signedVAA); + return getClaim( + connection, + nftBridgeAddress, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence, + commitment + ).catch((e) => false); } diff --git a/sdk/js/src/nft_bridge/getIsWrappedAsset.ts b/sdk/js/src/nft_bridge/getIsWrappedAsset.ts index 7cd1f4937..50472c0bc 100644 --- a/sdk/js/src/nft_bridge/getIsWrappedAsset.ts +++ b/sdk/js/src/nft_bridge/getIsWrappedAsset.ts @@ -1,46 +1,45 @@ -import { Connection, PublicKey } from "@solana/web3.js"; +import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js"; import { ethers } from "ethers"; import { Bridge__factory } from "../ethers-contracts"; -import { importNftWasm } from "../solana/wasm"; +import { getWrappedMeta } from "../solana/nftBridge"; /** * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param provider * @param assetAddress * @returns */ export async function getIsWrappedAssetEth( - tokenBridgeAddress: string, + nftBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, assetAddress: string ) { if (!assetAddress) return false; - const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); + const tokenBridge = Bridge__factory.connect(nftBridgeAddress, provider); return await tokenBridge.isWrappedAsset(assetAddress); } /** * Returns whether or not an asset on Solana is a wormhole wrapped asset * @param connection - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param mintAddress + * @param [commitment] * @returns */ -export async function getIsWrappedAssetSol( +export async function getIsWrappedAssetSolana( connection: Connection, - tokenBridgeAddress: string, - mintAddress: string + nftBridgeAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + commitment?: Commitment ) { - if (!mintAddress) return false; - const { wrapped_meta_address } = await importNftWasm(); - const wrappedMetaAddress = wrapped_meta_address( - tokenBridgeAddress, - new PublicKey(mintAddress).toBytes() - ); - const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress); - const wrappedMetaAccountInfo = await connection.getAccountInfo( - wrappedMetaAddressPK - ); - return !!wrappedMetaAccountInfo; + if (!mintAddress) { + return false; + } + return getWrappedMeta(connection, nftBridgeAddress, mintAddress, commitment) + .catch((_) => null) + .then((meta) => meta != null); } + +export const getIsWrappedAssetSol = getIsWrappedAssetSolana; diff --git a/sdk/js/src/nft_bridge/getOriginalAsset.ts b/sdk/js/src/nft_bridge/getOriginalAsset.ts index 0e46ce24c..e22fb72b6 100644 --- a/sdk/js/src/nft_bridge/getOriginalAsset.ts +++ b/sdk/js/src/nft_bridge/getOriginalAsset.ts @@ -1,10 +1,16 @@ -import { Connection, PublicKey } from "@solana/web3.js"; +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, +} from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import { BigNumber, ethers } from "ethers"; import { arrayify, zeroPad } from "ethers/lib/utils"; -import { canonicalAddress, WormholeWrappedInfo } from ".."; +import { WormholeWrappedInfo } from ".."; +import { canonicalAddress } from "../cosmos"; import { TokenImplementation__factory } from "../ethers-contracts"; -import { importNftWasm } from "../solana/wasm"; +import { getWrappedMeta } from "../solana/nftBridge"; import { ChainId, ChainName, @@ -24,20 +30,20 @@ export interface WormholeWrappedNFTInfo { /** * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param provider * @param wrappedAddress * @returns */ export async function getOriginalAssetEth( - tokenBridgeAddress: string, + nftBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, wrappedAddress: string, tokenId: string, lookupChain: ChainId | ChainName ): Promise { const isWrapped = await getIsWrappedAssetEth( - tokenBridgeAddress, + nftBridgeAddress, provider, wrappedAddress ); @@ -69,56 +75,49 @@ export async function getOriginalAssetEth( /** * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address * @param connection - * @param tokenBridgeAddress + * @param nftBridgeAddress * @param mintAddress + * @param [commitment] * @returns */ -export async function getOriginalAssetSol( +export async function getOriginalAssetSolana( connection: Connection, - tokenBridgeAddress: string, - mintAddress: string + nftBridgeAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + commitment?: Commitment ): Promise { - if (mintAddress) { - // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something - const { parse_wrapped_meta, wrapped_meta_address } = await importNftWasm(); - const wrappedMetaAddress = wrapped_meta_address( - tokenBridgeAddress, - new PublicKey(mintAddress).toBytes() - ); - const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress); - const wrappedMetaAccountInfo = await connection.getAccountInfo( - wrappedMetaAddressPK - ); - if (wrappedMetaAccountInfo) { - const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data); - const token_id_arr = parsed.token_id as BigUint64Array; - const token_id_bytes = []; - for (let elem of token_id_arr.reverse()) { - token_id_bytes.push(...bigToUint8Array(elem)); - } - const token_id = BigNumber.from(token_id_bytes).toString(); - return { - isWrapped: true, - chainId: parsed.chain, - assetAddress: parsed.token_address, - tokenId: token_id, - }; - } - } try { + const mint = new PublicKey(mintAddress); + + return getWrappedMeta(connection, nftBridgeAddress, mintAddress, commitment) + .catch((_) => null) + .then((meta) => { + if (meta === null) { + return { + isWrapped: false, + chainId: CHAIN_ID_SOLANA, + assetAddress: mint.toBytes(), + }; + } else { + return { + isWrapped: true, + chainId: meta.chain as ChainId, + assetAddress: Uint8Array.from(meta.tokenAddress), + tokenId: meta.tokenId.toString(), + }; + } + }); + } catch (_) { return { isWrapped: false, chainId: CHAIN_ID_SOLANA, - assetAddress: new PublicKey(mintAddress).toBytes(), + assetAddress: new Uint8Array(32), }; - } catch (e) {} - return { - isWrapped: false, - chainId: CHAIN_ID_SOLANA, - assetAddress: new Uint8Array(32), - }; + } } +export const getOriginalAssetSol = getOriginalAssetSolana; + // Derived from https://www.jackieli.dev/posts/bigint-to-uint8array/ const big0 = BigInt(0); const big1 = BigInt(1); diff --git a/sdk/js/src/nft_bridge/redeem.ts b/sdk/js/src/nft_bridge/redeem.ts index 18aee1cb6..e27555f63 100644 --- a/sdk/js/src/nft_bridge/redeem.ts +++ b/sdk/js/src/nft_bridge/redeem.ts @@ -1,19 +1,29 @@ -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + Transaction, +} from "@solana/web3.js"; import { MsgExecuteContract } from "@terra-money/terra.js"; import { ethers, Overrides } from "ethers"; import { fromUint8Array } from "js-base64"; import { CHAIN_ID_SOLANA } from ".."; import { Bridge__factory } from "../ethers-contracts"; -import { ixFromRust } from "../solana"; -import { importCoreWasm, importNftWasm } from "../solana/wasm"; +import { + createCompleteTransferNativeInstruction, + createCompleteTransferWrappedInstruction, + createCompleteWrappedMetaInstruction, +} from "../solana/nftBridge"; +import { parseNftTransferVaa, parseVaa, SignedVaa } from "../vaa"; export async function redeemOnEth( - tokenBridgeAddress: string, + nftBridgeAddress: string, signer: ethers.Signer, signedVAA: Uint8Array, overrides: Overrides & { from?: string | Promise } = {} ): Promise { - const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); + const bridge = Bridge__factory.connect(nftBridgeAddress, signer); const v = await bridge.completeTransfer(signedVAA, overrides); const receipt = await v.wait(); return receipt; @@ -22,52 +32,33 @@ export async function redeemOnEth( export async function isNFTVAASolanaNative( signedVAA: Uint8Array ): Promise { - const { parse_vaa } = await importCoreWasm(); - const parsedVAA = parse_vaa(signedVAA); - const isSolanaNative = - Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(33) === - CHAIN_ID_SOLANA; - return isSolanaNative; + return parseVaa(signedVAA).payload.readUInt16BE(33) === CHAIN_ID_SOLANA; } export async function redeemOnSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - signedVAA: Uint8Array + bridgeAddress: PublicKeyInitData, + nftBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + signedVaa: SignedVaa, + toAuthorityAddress?: PublicKeyInitData, + commitment?: Commitment ): Promise { - const isSolanaNative = await isNFTVAASolanaNative(signedVAA); - const { complete_transfer_wrapped_ix, complete_transfer_native_ix } = - await importNftWasm(); - const ixs = []; - if (isSolanaNative) { - ixs.push( - ixFromRust( - complete_transfer_native_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - payerAddress, //TODO: allow for a different address than payer - signedVAA - ) - ) - ); - } else { - ixs.push( - ixFromRust( - complete_transfer_wrapped_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - payerAddress, //TODO: allow for a different address than payer - signedVAA - ) - ) - ); - } - const transaction = new Transaction().add(...ixs); - const { blockhash } = await connection.getRecentBlockhash(); + const parsed = parseNftTransferVaa(signedVaa); + const createCompleteTransferInstruction = + parsed.tokenChain == CHAIN_ID_SOLANA + ? createCompleteTransferNativeInstruction + : createCompleteTransferWrappedInstruction; + const transaction = new Transaction().add( + createCompleteTransferInstruction( + nftBridgeAddress, + bridgeAddress, + payerAddress, + parsed, + toAuthorityAddress + ) + ); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); return transaction; @@ -75,33 +66,36 @@ export async function redeemOnSolana( export async function createMetaOnSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - signedVAA: Uint8Array + bridgeAddress: PublicKeyInitData, + nftBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + signedVaa: SignedVaa, + commitment?: Commitment ): Promise { - const { complete_transfer_wrapped_meta_ix } = await importNftWasm(); - const ix = ixFromRust( - complete_transfer_wrapped_meta_ix( - tokenBridgeAddress, + const parsed = parseNftTransferVaa(signedVaa); + if (parsed.tokenChain == CHAIN_ID_SOLANA) { + return Promise.reject("parsed.tokenChain == CHAIN_ID_SOLANA"); + } + const transaction = new Transaction().add( + createCompleteWrappedMetaInstruction( + nftBridgeAddress, bridgeAddress, payerAddress, - signedVAA + parsed ) ); - const transaction = new Transaction().add(ix); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); return transaction; } export async function redeemOnTerra( - tokenBridgeAddress: string, + nftBridgeAddress: string, walletAddress: string, signedVAA: Uint8Array ): Promise { - return new MsgExecuteContract(walletAddress, tokenBridgeAddress, { + return new MsgExecuteContract(walletAddress, nftBridgeAddress, { submit_vaa: { data: fromUint8Array(signedVAA), }, diff --git a/sdk/js/src/nft_bridge/transfer.ts b/sdk/js/src/nft_bridge/transfer.ts index 5aae70106..84858729a 100644 --- a/sdk/js/src/nft_bridge/transfer.ts +++ b/sdk/js/src/nft_bridge/transfer.ts @@ -1,17 +1,36 @@ -import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { createApproveInstruction } from "@solana/spl-token"; +import { + Commitment, + Connection, + Keypair, + PublicKey, + PublicKeyInitData, + Transaction, +} from "@solana/web3.js"; import { MsgExecuteContract } from "@terra-money/terra.js"; import { ethers, Overrides } from "ethers"; +import { BN } from "@project-serum/anchor"; import { NFTBridge__factory, NFTImplementation__factory, } from "../ethers-contracts"; -import { getBridgeFeeIx, ixFromRust } from "../solana"; -import { importNftWasm } from "../solana/wasm"; -import { ChainId, ChainName, CHAIN_ID_SOLANA, coalesceChainId, createNonce } from "../utils"; +import { createBridgeFeeTransferInstruction } from "../solana"; +import { + createApproveAuthoritySignerInstruction, + createTransferNativeInstruction, + createTransferWrappedInstruction, +} from "../solana/nftBridge"; +import { + ChainId, + ChainName, + CHAIN_ID_SOLANA, + coalesceChainId, + createNonce, +} from "../utils"; +import { isBytes } from "ethers/lib/utils"; export async function transferFromEth( - tokenBridgeAddress: string, + nftBridgeAddress: string, signer: ethers.Signer, tokenAddress: string, tokenID: ethers.BigNumberish, @@ -19,11 +38,11 @@ export async function transferFromEth( recipientAddress: Uint8Array, overrides: Overrides & { from?: string | Promise } = {} ): Promise { - const recipientChainId = coalesceChainId(recipientChain) + const recipientChainId = coalesceChainId(recipientChain); //TODO: should we check if token attestation exists on the target chain const token = NFTImplementation__factory.connect(tokenAddress, signer); - await (await token.approve(tokenBridgeAddress, tokenID, overrides)).wait(); - const bridge = NFTBridge__factory.connect(tokenBridgeAddress, signer); + await (await token.approve(nftBridgeAddress, tokenID, overrides)).wait(); + const bridge = NFTBridge__factory.connect(nftBridgeAddress, signer); const v = await bridge.transferNFT( tokenAddress, tokenID, @@ -38,90 +57,89 @@ export async function transferFromEth( export async function transferFromSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - fromAddress: string, - mintAddress: string, - targetAddress: Uint8Array, + bridgeAddress: PublicKeyInitData, + nftBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + fromAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + targetAddress: Uint8Array | Buffer, targetChain: ChainId | ChainName, - originAddress?: Uint8Array, + originAddress?: Uint8Array | Buffer, originChain?: ChainId | ChainName, - originTokenId?: Uint8Array + originTokenId?: Uint8Array | Buffer | number | bigint, + commitment?: Commitment ): Promise { - const originChainId: ChainId | undefined = originChain ? coalesceChainId(originChain) : undefined + const originChainId: ChainId | undefined = originChain + ? coalesceChainId(originChain) + : undefined; const nonce = createNonce().readUInt32LE(0); - const transferIx = await getBridgeFeeIx( + const transferIx = await createBridgeFeeTransferInstruction( connection, bridgeAddress, payerAddress ); - const { - transfer_native_ix, - transfer_wrapped_ix, - approval_authority_address, - } = await importNftWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(fromAddress), - new PublicKey(approval_authority_address(tokenBridgeAddress)), - new PublicKey(payerAddress), - [], - Number(1) + const approvalIx = createApproveAuthoritySignerInstruction( + nftBridgeAddress, + fromAddress, + payerAddress ); - let messageKey = Keypair.generate(); + let message = Keypair.generate(); const isSolanaNative = originChain === undefined || originChain === CHAIN_ID_SOLANA; if (!isSolanaNative && (!originAddress || !originTokenId)) { - throw new Error( + return Promise.reject( "originAddress and originTokenId are required when specifying originChain" ); } - const ix = ixFromRust( - isSolanaNative - ? transfer_native_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - messageKey.publicKey.toString(), - fromAddress, - mintAddress, - nonce, - targetAddress, - coalesceChainId(targetChain) - ) - : transfer_wrapped_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - messageKey.publicKey.toString(), - fromAddress, - payerAddress, - originChainId as number, // checked by isSolanaNative - originAddress as Uint8Array, // checked by throw - originTokenId as Uint8Array, // checked by throw - nonce, - targetAddress, - coalesceChainId(targetChain) - ) + const nftBridgeTransferIx = isSolanaNative + ? createTransferNativeInstruction( + nftBridgeAddress, + bridgeAddress, + payerAddress, + message.publicKey, + fromAddress, + mintAddress, + nonce, + targetAddress, + coalesceChainId(targetChain) + ) + : createTransferWrappedInstruction( + nftBridgeAddress, + bridgeAddress, + payerAddress, + message.publicKey, + fromAddress, + payerAddress, + originChainId!, + originAddress!, + isBytes(originTokenId) + ? BigInt(new BN(originTokenId).toString()) + : originTokenId!, + nonce, + targetAddress, + coalesceChainId(targetChain) + ); + const transaction = new Transaction().add( + transferIx, + approvalIx, + nftBridgeTransferIx ); - const transaction = new Transaction().add(transferIx, approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); - transaction.partialSign(messageKey); + transaction.partialSign(message); return transaction; } export async function transferFromTerra( walletAddress: string, - tokenBridgeAddress: string, + nftBridgeAddress: string, tokenAddress: string, tokenID: string, recipientChain: ChainId | ChainName, recipientAddress: Uint8Array ): Promise { - const recipientChainId = coalesceChainId(recipientChain) + const recipientChainId = coalesceChainId(recipientChain); const nonce = Math.round(Math.random() * 100000); return [ new MsgExecuteContract( @@ -129,7 +147,7 @@ export async function transferFromTerra( tokenAddress, { approve: { - spender: tokenBridgeAddress, + spender: nftBridgeAddress, token_id: tokenID, }, }, @@ -137,7 +155,7 @@ export async function transferFromTerra( ), new MsgExecuteContract( walletAddress, - tokenBridgeAddress, + nftBridgeAddress, { initiate_transfer: { contract_addr: tokenAddress, diff --git a/sdk/js/src/solana/anchor/common.ts b/sdk/js/src/solana/anchor/common.ts new file mode 100644 index 000000000..27056b69a --- /dev/null +++ b/sdk/js/src/solana/anchor/common.ts @@ -0,0 +1,97 @@ +// Borrowed from coral-xyz/anchor +// +// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/coder/common.ts + +import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "./idl"; +import { IdlError } from "./error"; + +export function accountSize(idl: Idl, idlAccount: IdlTypeDef): number { + if (idlAccount.type.kind === "enum") { + let variantSizes = idlAccount.type.variants.map( + (variant: IdlEnumVariant) => { + if (variant.fields === undefined) { + return 0; + } + return variant.fields + .map((f: IdlField | IdlType) => { + if (!(typeof f === "object" && "name" in f)) { + throw new Error("Tuple enum variants not yet implemented."); + } + return typeSize(idl, f.type); + }) + .reduce((a: number, b: number) => a + b); + } + ); + return Math.max(...variantSizes) + 1; + } + if (idlAccount.type.fields === undefined) { + return 0; + } + return idlAccount.type.fields + .map((f) => typeSize(idl, f.type)) + .reduce((a, b) => a + b, 0); +} + +// Returns the size of the type in bytes. For variable length types, just return +// 1. Users should override this value in such cases. +function typeSize(idl: Idl, ty: IdlType): number { + switch (ty) { + case "bool": + return 1; + case "u8": + return 1; + case "i8": + return 1; + case "i16": + return 2; + case "u16": + return 2; + case "u32": + return 4; + case "i32": + return 4; + case "f32": + return 4; + case "u64": + return 8; + case "i64": + return 8; + case "f64": + return 8; + case "u128": + return 16; + case "i128": + return 16; + case "bytes": + return 1; + case "string": + return 1; + case "publicKey": + return 32; + default: + if ("vec" in ty) { + return 1; + } + if ("option" in ty) { + return 1 + typeSize(idl, ty.option); + } + if ("coption" in ty) { + return 4 + typeSize(idl, ty.coption); + } + if ("defined" in ty) { + const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? []; + if (filtered.length !== 1) { + throw new IdlError(`Type not found: ${JSON.stringify(ty)}`); + } + let typeDef = filtered[0]; + + return accountSize(idl, typeDef); + } + if ("array" in ty) { + let arrayTy = ty.array[0]; + let arraySize = ty.array[1]; + return typeSize(idl, arrayTy) * arraySize; + } + throw new Error(`Invalid type ${JSON.stringify(ty)}`); + } +} diff --git a/sdk/js/src/solana/anchor/error.ts b/sdk/js/src/solana/anchor/error.ts new file mode 100644 index 000000000..6f88858d1 --- /dev/null +++ b/sdk/js/src/solana/anchor/error.ts @@ -0,0 +1,10 @@ +// Borrowed from coral-xyz/anchor +// +// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/error.ts + +export class IdlError extends Error { + constructor(message: string) { + super(message); + this.name = "IdlError"; + } +} diff --git a/sdk/js/src/solana/anchor/idl.ts b/sdk/js/src/solana/anchor/idl.ts new file mode 100644 index 000000000..7d3f39bab --- /dev/null +++ b/sdk/js/src/solana/anchor/idl.ts @@ -0,0 +1,204 @@ +// Borrowed from coral-xyz/anchor +// +// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/idl.ts + +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import * as borsh from "@project-serum/borsh"; + +export type Idl = { + version: string; + name: string; + docs?: string[]; + instructions: IdlInstruction[]; + state?: IdlState; + accounts?: IdlAccountDef[]; + types?: IdlTypeDef[]; + events?: IdlEvent[]; + errors?: IdlErrorCode[]; + constants?: IdlConstant[]; + metadata?: IdlMetadata; +}; + +export type IdlMetadata = any; + +export type IdlConstant = { + name: string; + type: IdlType; + value: string; +}; + +export type IdlEvent = { + name: string; + fields: IdlEventField[]; +}; + +export type IdlEventField = { + name: string; + type: IdlType; + index: boolean; +}; + +export type IdlInstruction = { + name: string; + docs?: string[]; + accounts: IdlAccountItem[]; + args: IdlField[]; + returns?: IdlType; +}; + +export type IdlState = { + struct: IdlTypeDef; + methods: IdlStateMethod[]; +}; + +export type IdlStateMethod = IdlInstruction; + +export type IdlAccountItem = IdlAccount | IdlAccounts; + +export type IdlAccount = { + name: string; + isMut: boolean; + isSigner: boolean; + docs?: string[]; + pda?: IdlPda; +}; + +export type IdlPda = { + seeds: IdlSeed[]; + programId?: IdlSeed; +}; + +export type IdlSeed = any; // TODO + +// A nested/recursive version of IdlAccount. +export type IdlAccounts = { + name: string; + docs?: string[]; + accounts: IdlAccountItem[]; +}; + +export type IdlField = { + name: string; + docs?: string[]; + type: IdlType; +}; + +export type IdlTypeDef = { + name: string; + docs?: string[]; + type: IdlTypeDefTy; +}; + +export type IdlAccountDef = { + name: string; + docs?: string[]; + type: IdlTypeDefTyStruct; +}; + +export type IdlTypeDefTyStruct = { + kind: "struct"; + fields: IdlTypeDefStruct; +}; + +export type IdlTypeDefTyEnum = { + kind: "enum"; + variants: IdlEnumVariant[]; +}; + +export type IdlTypeDefTy = IdlTypeDefTyEnum | IdlTypeDefTyStruct; + +export type IdlTypeDefStruct = Array; + +export type IdlType = + | "bool" + | "u8" + | "i8" + | "u16" + | "i16" + | "u32" + | "i32" + | "f32" + | "u64" + | "i64" + | "f64" + | "u128" + | "i128" + | "bytes" + | "string" + | "publicKey" + | IdlTypeDefined + | IdlTypeOption + | IdlTypeCOption + | IdlTypeVec + | IdlTypeArray; + +// User defined type. +export type IdlTypeDefined = { + defined: string; +}; + +export type IdlTypeOption = { + option: IdlType; +}; + +export type IdlTypeCOption = { + coption: IdlType; +}; + +export type IdlTypeVec = { + vec: IdlType; +}; + +export type IdlTypeArray = { + array: [idlType: IdlType, size: number]; +}; + +export type IdlEnumVariant = { + name: string; + fields?: IdlEnumFields; +}; + +export type IdlEnumFields = IdlEnumFieldsNamed | IdlEnumFieldsTuple; + +export type IdlEnumFieldsNamed = IdlField[]; + +export type IdlEnumFieldsTuple = IdlType[]; + +export type IdlErrorCode = { + code: number; + name: string; + msg?: string; +}; + +// Deterministic IDL address as a function of the program id. +export async function idlAddress(programId: PublicKey): Promise { + const base = (await PublicKey.findProgramAddress([], programId))[0]; + return await PublicKey.createWithSeed(base, seed(), programId); +} + +// Seed for generating the idlAddress. +export function seed(): string { + return "anchor:idl"; +} + +// The on-chain account of the IDL. +export interface IdlProgramAccount { + authority: PublicKey; + data: Buffer; +} + +const IDL_ACCOUNT_LAYOUT: borsh.Layout = borsh.struct([ + borsh.publicKey("authority"), + borsh.vecU8("data"), +]); + +export function decodeIdlAccount(data: Buffer): IdlProgramAccount { + return IDL_ACCOUNT_LAYOUT.decode(data); +} + +export function encodeIdlAccount(acc: IdlProgramAccount): Buffer { + const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer. + const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer); + return buffer.slice(0, len); +} diff --git a/sdk/js/src/solana/anchor/index.ts b/sdk/js/src/solana/anchor/index.ts new file mode 100644 index 000000000..1acbf87b8 --- /dev/null +++ b/sdk/js/src/solana/anchor/index.ts @@ -0,0 +1,3 @@ +export * from "./common"; +export * from "./error"; +export * from "./idl"; diff --git a/sdk/js/src/solana/getBridgeFeeIx.ts b/sdk/js/src/solana/getBridgeFeeIx.ts deleted file mode 100644 index 0a2067dc5..000000000 --- a/sdk/js/src/solana/getBridgeFeeIx.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Connection, PublicKey, SystemProgram } from "@solana/web3.js"; -import { importCoreWasm } from "./wasm"; - -export async function getBridgeFeeIx( - connection: Connection, - bridgeAddress: string, - payerAddress: string -) { - const bridge = await importCoreWasm(); - const feeAccount = await bridge.fee_collector_address(bridgeAddress); - const bridgeStatePK = new PublicKey(bridge.state_address(bridgeAddress)); - const bridgeStateAccountInfo = await connection.getAccountInfo(bridgeStatePK); - if (bridgeStateAccountInfo?.data === undefined) { - throw new Error("bridge state not found"); - } - const bridgeState = bridge.parse_state( - new Uint8Array(bridgeStateAccountInfo?.data) - ); - const transferIx = SystemProgram.transfer({ - fromPubkey: new PublicKey(payerAddress), - toPubkey: new PublicKey(feeAccount), - lamports: bridgeState.config.fee, - }); - return transferIx; -} diff --git a/sdk/js/src/solana/index.ts b/sdk/js/src/solana/index.ts index 9fd69ee14..1e7685637 100644 --- a/sdk/js/src/solana/index.ts +++ b/sdk/js/src/solana/index.ts @@ -1,9 +1,15 @@ -export * from "./getBridgeFeeIx"; +export * from "./utils"; + export { - createPostVaaInstruction as createPostVaaInstructionSolana, - createVerifySignaturesInstructions as createVerifySignaturesInstructionsSolana, postVaa as postVaaSolana, postVaaWithRetry as postVaaSolanaWithRetry, -} from "./postVaa"; -export * from "./rust"; -export * from "./wasm"; +} from "./sendAndConfirmPostVaa"; +export { + createVerifySignaturesInstructions as createVerifySignaturesInstructionsSolana, + createPostVaaInstruction as createPostVaaInstructionSolana, + createBridgeFeeTransferInstruction, + getPostMessageAccounts as getWormholeCpiAccounts, +} from "./wormhole"; + +export * from "./wormhole/cpi"; +export * from "./tokenBridge/cpi"; diff --git a/sdk/js/src/solana/nftBridge/accounts/config.ts b/sdk/js/src/solana/nftBridge/accounts/config.ts new file mode 100644 index 000000000..0ff9f0949 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/accounts/config.ts @@ -0,0 +1,18 @@ +import { Connection, Commitment, PublicKeyInitData } from "@solana/web3.js"; +import { + deriveTokenBridgeConfigKey, + getTokenBridgeConfig, + TokenBridgeConfig, +} from "../../tokenBridge"; + +export const deriveNftBridgeConfigKey = deriveTokenBridgeConfigKey; + +export async function getNftBridgeConfig( + connection: Connection, + nftBridgeProgramId: PublicKeyInitData, + commitment?: Commitment +): Promise { + return getTokenBridgeConfig(connection, nftBridgeProgramId, commitment); +} + +export class NftBridgeConfig extends TokenBridgeConfig {} diff --git a/sdk/js/src/solana/nftBridge/accounts/index.ts b/sdk/js/src/solana/nftBridge/accounts/index.ts new file mode 100644 index 000000000..2a87b264e --- /dev/null +++ b/sdk/js/src/solana/nftBridge/accounts/index.ts @@ -0,0 +1,13 @@ +export * from "./config"; +export * from "./wrapped"; + +export { + EndpointRegistration, + deriveAuthoritySignerKey, + deriveCustodyKey, + deriveCustodySignerKey, + deriveEndpointKey, + deriveMintAuthorityKey, + deriveUpgradeAuthorityKey, + getEndpointRegistration, +} from "../../tokenBridge"; diff --git a/sdk/js/src/solana/nftBridge/accounts/wrapped.ts b/sdk/js/src/solana/nftBridge/accounts/wrapped.ts new file mode 100644 index 000000000..e6e5d3338 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/accounts/wrapped.ts @@ -0,0 +1,83 @@ +import { BN } from "@project-serum/anchor"; +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { + ChainId, + CHAIN_ID_SOLANA, + tryNativeToUint8Array, +} from "../../../utils"; +import { deriveAddress, getAccountData } from "../../utils"; +import { deriveWrappedMetaKey } from "../../tokenBridge"; + +export { deriveWrappedMetaKey } from "../../tokenBridge"; + +export function deriveWrappedMintKey( + tokenBridgeProgramId: PublicKeyInitData, + tokenChain: number | ChainId, + tokenAddress: Buffer | Uint8Array | string, + tokenId: bigint | number +): PublicKey { + if (tokenChain == CHAIN_ID_SOLANA) { + throw new Error( + "tokenChain == CHAIN_ID_SOLANA does not have wrapped mint key" + ); + } + if (typeof tokenAddress == "string") { + tokenAddress = tryNativeToUint8Array(tokenAddress, tokenChain as ChainId); + } + return deriveAddress( + [ + Buffer.from("wrapped"), + (() => { + const buf = Buffer.alloc(2); + buf.writeUInt16BE(tokenChain as number); + return buf; + })(), + tokenAddress, + new BN(tokenId.toString()).toBuffer("be", 32), + ], + tokenBridgeProgramId + ); +} + +export async function getWrappedMeta( + connection: Connection, + tokenBridgeProgramId: PublicKeyInitData, + mint: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo( + deriveWrappedMetaKey(tokenBridgeProgramId, mint), + commitment + ) + .then((info) => WrappedMeta.deserialize(getAccountData(info))); +} + +export class WrappedMeta { + chain: number; + tokenAddress: Buffer; + tokenId: bigint; + + constructor(chain: number, tokenAddress: Buffer, tokenId: bigint) { + this.chain = chain; + this.tokenAddress = tokenAddress; + this.tokenId = tokenId; + } + + static deserialize(data: Buffer): WrappedMeta { + if (data.length != 66) { + throw new Error("data.length != 66"); + } + const chain = data.readUInt16LE(0); + const tokenAddress = data.subarray(2, 34); + const tokenId = BigInt( + new BN(data.subarray(34, 66), undefined, "le").toString() + ); + return new WrappedMeta(chain, tokenAddress, tokenId); + } +} diff --git a/sdk/js/src/solana/nftBridge/coder/accounts.ts b/sdk/js/src/solana/nftBridge/coder/accounts.ts new file mode 100644 index 000000000..f154f853e --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/accounts.ts @@ -0,0 +1,40 @@ +import { AccountsCoder, Idl } from "@project-serum/anchor"; +import { accountSize, IdlTypeDef } from "../../anchor"; + +export class NftBridgeAccountsCoder + implements AccountsCoder +{ + constructor(private idl: Idl) {} + + public async encode(accountName: A, account: T): Promise { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public decode(accountName: A, ix: Buffer): T { + return this.decodeUnchecked(accountName, ix); + } + + public decodeUnchecked(accountName: A, ix: Buffer): T { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public memcmp(accountName: A, _appendData?: Buffer): any { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public size(idlAccount: IdlTypeDef): number { + return accountSize(this.idl, idlAccount) ?? 0; + } +} diff --git a/sdk/js/src/solana/nftBridge/coder/events.ts b/sdk/js/src/solana/nftBridge/coder/events.ts new file mode 100644 index 000000000..85b005bb7 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/events.ts @@ -0,0 +1,12 @@ +import { EventCoder, Event, Idl } from "@project-serum/anchor"; +import { IdlEvent } from "../../anchor"; + +export class NftBridgeEventsCoder implements EventCoder { + constructor(_idl: Idl) {} + + decode>( + _log: string + ): Event | null { + throw new Error("NFT Bridge program does not have events"); + } +} diff --git a/sdk/js/src/solana/nftBridge/coder/index.ts b/sdk/js/src/solana/nftBridge/coder/index.ts new file mode 100644 index 000000000..3ad0d7854 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/index.ts @@ -0,0 +1,24 @@ +import { Coder, Idl } from "@project-serum/anchor"; +import { NftBridgeAccountsCoder } from "./accounts"; +import { NftBridgeEventsCoder } from "./events"; +import { NftBridgeInstructionCoder } from "./instruction"; +import { NftBridgeStateCoder } from "./state"; +import { NftBridgeTypesCoder } from "./types"; + +export { NftBridgeInstruction } from "./instruction"; + +export class NftBridgeCoder implements Coder { + readonly instruction: NftBridgeInstructionCoder; + readonly accounts: NftBridgeAccountsCoder; + readonly state: NftBridgeStateCoder; + readonly events: NftBridgeEventsCoder; + readonly types: NftBridgeTypesCoder; + + constructor(idl: Idl) { + this.instruction = new NftBridgeInstructionCoder(idl); + this.accounts = new NftBridgeAccountsCoder(idl); + this.state = new NftBridgeStateCoder(idl); + this.events = new NftBridgeEventsCoder(idl); + this.types = new NftBridgeTypesCoder(idl); + } +} diff --git a/sdk/js/src/solana/nftBridge/coder/instruction.ts b/sdk/js/src/solana/nftBridge/coder/instruction.ts new file mode 100644 index 000000000..f52d3b4b2 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/instruction.ts @@ -0,0 +1,130 @@ +import { Idl, InstructionCoder } from "@project-serum/anchor"; +import { PublicKey } from "@solana/web3.js"; + +export class NftBridgeInstructionCoder implements InstructionCoder { + constructor(_: Idl) {} + + encode(ixName: string, ix: any): Buffer { + switch (ixName) { + case "initialize": { + return encodeInitialize(ix); + } + case "completeNative": { + return encodeCompleteNative(ix); + } + case "completeWrapped": { + return encodeCompleteWrapped(ix); + } + case "completeWrappedMeta": { + return encodeCompleteWrappedMeta(ix); + } + case "transferWrapped": { + return encodeTransferWrapped(ix); + } + case "transferNative": { + return encodeTransferNative(ix); + } + case "registerChain": { + return encodeRegisterChain(ix); + } + case "upgradeContract": { + return encodeUpgradeContract(ix); + } + default: { + throw new Error(`Invalid instruction: ${ixName}`); + } + } + } + + encodeState(_ixName: string, _ix: any): Buffer { + throw new Error("NFT Bridge program does not have state"); + } +} + +/** Solitaire enum of existing the NFT Bridge's instructions. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/modules/nft_bridge/program/src/lib.rs#L74 + */ +export enum NftBridgeInstruction { + Initialize, + CompleteNative, + CompleteWrapped, + CompleteWrappedMeta, + TransferWrapped, + TransferNative, + RegisterChain, + UpgradeContract, +} + +function encodeNftBridgeInstructionData( + instructionType: NftBridgeInstruction, + data?: Buffer +): Buffer { + const dataLen = data === undefined ? 0 : data.length; + const instructionData = Buffer.alloc(1 + dataLen); + instructionData.writeUInt8(instructionType, 0); + if (dataLen > 0) { + instructionData.write(data!.toString("hex"), 1, "hex"); + } + return instructionData; +} + +function encodeInitialize({ wormhole }: any): Buffer { + const serialized = Buffer.alloc(32); + serialized.write( + new PublicKey(wormhole).toBuffer().toString("hex"), + 0, + "hex" + ); + return encodeNftBridgeInstructionData( + NftBridgeInstruction.Initialize, + serialized + ); +} + +function encodeCompleteNative({}: any) { + return encodeNftBridgeInstructionData(NftBridgeInstruction.CompleteNative); +} + +function encodeCompleteWrapped({}: any) { + return encodeNftBridgeInstructionData(NftBridgeInstruction.CompleteWrapped); +} + +function encodeCompleteWrappedMeta({}: any) { + return encodeNftBridgeInstructionData( + NftBridgeInstruction.CompleteWrappedMeta + ); +} + +function encodeTransferData({ nonce, targetAddress, targetChain }: any) { + if (!Buffer.isBuffer(targetAddress)) { + throw new Error("targetAddress must be Buffer"); + } + const serialized = Buffer.alloc(38); + serialized.writeUInt32LE(nonce, 0); + serialized.write(targetAddress.toString("hex"), 4, "hex"); + serialized.writeUInt16LE(targetChain, 36); + return serialized; +} + +function encodeTransferWrapped({ nonce, targetAddress, targetChain }: any) { + return encodeNftBridgeInstructionData( + NftBridgeInstruction.TransferWrapped, + encodeTransferData({ nonce, targetAddress, targetChain }) + ); +} + +function encodeTransferNative({ nonce, targetAddress, targetChain }: any) { + return encodeNftBridgeInstructionData( + NftBridgeInstruction.TransferNative, + encodeTransferData({ nonce, targetAddress, targetChain }) + ); +} + +function encodeRegisterChain({}: any) { + return encodeNftBridgeInstructionData(NftBridgeInstruction.RegisterChain); +} + +function encodeUpgradeContract({}: any) { + return encodeNftBridgeInstructionData(NftBridgeInstruction.UpgradeContract); +} diff --git a/sdk/js/src/solana/nftBridge/coder/state.ts b/sdk/js/src/solana/nftBridge/coder/state.ts new file mode 100644 index 000000000..25fe849bd --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/state.ts @@ -0,0 +1,12 @@ +import { Idl, StateCoder } from "@project-serum/anchor"; + +export class NftBridgeStateCoder implements StateCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _account: T): Promise { + throw new Error("NFT Bridge program does not have state"); + } + decode(_ix: Buffer): T { + throw new Error("NFT Bridge program does not have state"); + } +} diff --git a/sdk/js/src/solana/nftBridge/coder/types.ts b/sdk/js/src/solana/nftBridge/coder/types.ts new file mode 100644 index 000000000..3c68f7c1f --- /dev/null +++ b/sdk/js/src/solana/nftBridge/coder/types.ts @@ -0,0 +1,12 @@ +import { Idl, TypesCoder } from "@project-serum/anchor"; + +export class NftBridgeTypesCoder implements TypesCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _type: T): Buffer { + throw new Error("NFT Bridge program does not have user-defined types"); + } + decode(_name: string, _typeData: Buffer): T { + throw new Error("NFT Bridge program does not have user-defined types"); + } +} diff --git a/sdk/js/src/solana/nftBridge/index.ts b/sdk/js/src/solana/nftBridge/index.ts new file mode 100644 index 000000000..d8a01fd79 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/index.ts @@ -0,0 +1,3 @@ +export * from "./accounts"; +export * from "./instructions"; +export * from "./program"; diff --git a/sdk/js/src/solana/nftBridge/instructions/approve.ts b/sdk/js/src/solana/nftBridge/instructions/approve.ts new file mode 100644 index 000000000..c126bbbe1 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/approve.ts @@ -0,0 +1,15 @@ +import { PublicKeyInitData } from "@solana/web3.js"; +import { createApproveAuthoritySignerInstruction as _createApproveAuthoritySignerInstruction } from "../../tokenBridge"; + +export function createApproveAuthoritySignerInstruction( + nftBridgeProgramId: PublicKeyInitData, + tokenAccount: PublicKeyInitData, + owner: PublicKeyInitData +) { + return _createApproveAuthoritySignerInstruction( + nftBridgeProgramId, + tokenAccount, + owner, + 1 + ); +} diff --git a/sdk/js/src/solana/nftBridge/instructions/completeNative.ts b/sdk/js/src/solana/nftBridge/instructions/completeNative.ts new file mode 100644 index 000000000..8d5727341 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/completeNative.ts @@ -0,0 +1,107 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + createReadOnlyNftBridgeProgramInterface, + tokenIdToMint, +} from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveNftBridgeConfigKey, + deriveCustodyKey, + deriveCustodySignerKey, +} from "../accounts"; +import { + isBytes, + ParsedNftTransferVaa, + parseNftTransferVaa, + SignedVaa, +} from "../../../vaa"; + +export function createCompleteTransferNativeInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa, + toAuthority?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.completeNative(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCompleteTransferNativeAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + vaa, + toAuthority + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CompleteTransferNativeAccounts { + payer: PublicKey; + config: PublicKey; + vaa: PublicKey; + claim: PublicKey; + endpoint: PublicKey; + to: PublicKey; + toAuthority: PublicKey; + custody: PublicKey; + mint: PublicKey; + custodySigner: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCompleteTransferNativeAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa, + toAuthority?: PublicKeyInitData +): CompleteTransferNativeAccounts { + const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa; + // the mint key is encoded in the tokenId when it was transferred out + const mint = tokenIdToMint(parsed.tokenId); + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + nftBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + endpoint: deriveEndpointKey( + nftBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + to: new PublicKey(parsed.to), + toAuthority: new PublicKey(toAuthority === undefined ? payer : toAuthority), + custody: deriveCustodyKey(nftBridgeProgramId, mint), + mint, + custodySigner: deriveCustodySignerKey(nftBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts b/sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts new file mode 100644 index 000000000..fbb61b220 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/completeWrapped.ts @@ -0,0 +1,117 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveNftBridgeConfigKey, + deriveWrappedMintKey, + deriveWrappedMetaKey, + deriveMintAuthorityKey, +} from "../accounts"; +import { + isBytes, + ParsedNftTransferVaa, + parseNftTransferVaa, + SignedVaa, +} from "../../../vaa"; +import { SplTokenMetadataProgram } from "../../utils"; + +export function createCompleteTransferWrappedInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa, + toAuthority?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.completeWrapped(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCompleteTransferWrappedAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + vaa, + toAuthority + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CompleteTransferWrappedAccounts { + payer: PublicKey; + config: PublicKey; + vaa: PublicKey; + claim: PublicKey; + endpoint: PublicKey; + to: PublicKey; + toAuthority: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + mintAuthority: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + splMetadataProgram: PublicKey; + associatedTokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCompleteTransferWrappedAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa, + toAuthority?: PublicKeyInitData +): CompleteTransferWrappedAccounts { + const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa; + const mint = deriveWrappedMintKey( + nftBridgeProgramId, + parsed.tokenChain, + parsed.tokenAddress, + parsed.tokenId + ); + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + nftBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + endpoint: deriveEndpointKey( + nftBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + to: new PublicKey(parsed.to), + toAuthority: new PublicKey(toAuthority === undefined ? payer : toAuthority), + mint, + wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint), + mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + splMetadataProgram: SplTokenMetadataProgram.programId, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts b/sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts new file mode 100644 index 000000000..68278445c --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/completeWrappedMeta.ts @@ -0,0 +1,103 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveNftBridgeConfigKey, + deriveWrappedMintKey, + deriveWrappedMetaKey, + deriveMintAuthorityKey, +} from "../accounts"; +import { + isBytes, + ParsedNftTransferVaa, + parseNftTransferVaa, + SignedVaa, +} from "../../../vaa"; +import { + deriveSplTokenMetadataKey, + SplTokenMetadataProgram, +} from "../../utils"; + +export function createCompleteWrappedMetaInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa +): TransactionInstruction { + const methods = + createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.completeWrappedMeta(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCompleteWrappedMetaAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CompleteWrappedMetaAccounts { + payer: PublicKey; + config: PublicKey; + vaa: PublicKey; + endpoint: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + splMetadata: PublicKey; + mintAuthority: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + splMetadataProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCompleteWrappedMetaAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftTransferVaa +): CompleteWrappedMetaAccounts { + const parsed = isBytes(vaa) ? parseNftTransferVaa(vaa) : vaa; + const mint = deriveWrappedMintKey( + nftBridgeProgramId, + parsed.tokenChain, + parsed.tokenAddress, + parsed.tokenId + ); + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + endpoint: deriveEndpointKey( + nftBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + mint, + wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint), + splMetadata: deriveSplTokenMetadataKey(mint), + mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + splMetadataProgram: SplTokenMetadataProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/governance.ts b/sdk/js/src/solana/nftBridge/instructions/governance.ts new file mode 100644 index 000000000..76b212dd8 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/governance.ts @@ -0,0 +1,161 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveNftBridgeConfigKey, + deriveUpgradeAuthorityKey, +} from "../accounts"; +import { + isBytes, + ParsedNftBridgeRegisterChainVaa, + ParsedNftBridgeUpgradeContractVaa, + parseNftBridgeRegisterChainVaa, + parseNftBridgeUpgradeContractVaa, + SignedVaa, +} from "../../../vaa"; +import { BpfLoaderUpgradeable, deriveUpgradeableProgramKey } from "../../utils"; + +export function createRegisterChainInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftBridgeRegisterChainVaa +): TransactionInstruction { + const methods = + createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.registerChain(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getRegisterChainAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface RegisterChainAccounts { + payer: PublicKey; + config: PublicKey; + endpoint: PublicKey; + vaa: PublicKey; + claim: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getRegisterChainAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftBridgeRegisterChainVaa +): RegisterChainAccounts { + const parsed = isBytes(vaa) ? parseNftBridgeRegisterChainVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + endpoint: deriveEndpointKey( + nftBridgeProgramId, + parsed.foreignChain, + parsed.foreignAddress + ), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + nftBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} + +export function createUpgradeContractInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftBridgeUpgradeContractVaa, + spill?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.upgradeContract(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getUpgradeContractAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + vaa, + spill + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface UpgradeContractAccounts { + payer: PublicKey; + vaa: PublicKey; + claim: PublicKey; + upgradeAuthority: PublicKey; + spill: PublicKey; + implementation: PublicKey; + programData: PublicKey; + nftBridgeProgram: PublicKey; + rent: PublicKey; + clock: PublicKey; + bpfLoaderUpgradeable: PublicKey; + systemProgram: PublicKey; +} + +export function getUpgradeContractAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedNftBridgeUpgradeContractVaa, + spill?: PublicKeyInitData +): UpgradeContractAccounts { + const parsed = isBytes(vaa) ? parseNftBridgeUpgradeContractVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + nftBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + upgradeAuthority: deriveUpgradeAuthorityKey(nftBridgeProgramId), + spill: new PublicKey(spill === undefined ? payer : spill), + implementation: new PublicKey(parsed.newContract), + programData: deriveUpgradeableProgramKey(nftBridgeProgramId), + nftBridgeProgram: new PublicKey(nftBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + bpfLoaderUpgradeable: BpfLoaderUpgradeable.programId, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/index.ts b/sdk/js/src/solana/nftBridge/instructions/index.ts new file mode 100644 index 000000000..77fc7538a --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/index.ts @@ -0,0 +1,8 @@ +export * from "./approve"; +export * from "./completeNative"; +export * from "./completeWrapped"; +export * from "./completeWrappedMeta"; +export * from "./initialize"; +export * from "./governance"; +export * from "./transferNative"; +export * from "./transferWrapped"; diff --git a/sdk/js/src/solana/nftBridge/instructions/initialize.ts b/sdk/js/src/solana/nftBridge/instructions/initialize.ts new file mode 100644 index 000000000..c2701bee4 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/initialize.ts @@ -0,0 +1,47 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { deriveNftBridgeConfigKey } from "../accounts"; + +export function createInitializeInstruction( + nftBridgeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): TransactionInstruction { + const methods = createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.initialize(wormholeProgramId as any); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getInitializeAccounts(nftBridgeProgramId, payer) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface InitializeAccounts { + payer: PublicKey; + config: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getInitializeAccounts( + nftBridgeProgramId: PublicKeyInitData, + payer: PublicKeyInitData +): InitializeAccounts { + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/transferNative.ts b/sdk/js/src/solana/nftBridge/instructions/transferNative.ts new file mode 100644 index 000000000..2e1e44299 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/transferNative.ts @@ -0,0 +1,122 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { getPostMessageAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveCustodySignerKey, + deriveNftBridgeConfigKey, + deriveCustodyKey, +} from "../accounts"; +import { + deriveSplTokenMetadataKey, + SplTokenMetadataProgram, +} from "../../utils"; + +export function createTransferNativeInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData, + nonce: number, + targetAddress: Buffer | Uint8Array, + targetChain: number +): TransactionInstruction { + const methods = createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.transferNative( + nonce, + Buffer.from(targetAddress) as any, + targetChain + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferNativeAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + mint + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferNativeAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + mint: PublicKey; + splMetadata: PublicKey; + custody: PublicKey; + authoritySigner: PublicKey; + custodySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + splMetadataProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getTransferNativeAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData +): TransferNativeAccounts { + const { + bridge: wormholeBridge, + message: wormholeMessage, + emitter: wormholeEmitter, + sequence: wormholeSequence, + feeCollector: wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageAccounts( + wormholeProgramId, + payer, + nftBridgeProgramId, + message + ); + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + from: new PublicKey(from), + mint: new PublicKey(mint), + splMetadata: deriveSplTokenMetadataKey(mint), + custody: deriveCustodyKey(nftBridgeProgramId, mint), + authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId), + custodySigner: deriveCustodySignerKey(nftBridgeProgramId), + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + tokenProgram: TOKEN_PROGRAM_ID, + splMetadataProgram: SplTokenMetadataProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts b/sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts new file mode 100644 index 000000000..9f57d47bd --- /dev/null +++ b/sdk/js/src/solana/nftBridge/instructions/transferWrapped.ts @@ -0,0 +1,137 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyNftBridgeProgramInterface } from "../program"; +import { getPostMessageAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveNftBridgeConfigKey, + deriveWrappedMetaKey, + deriveWrappedMintKey, +} from "../accounts"; +import { + deriveSplTokenMetadataKey, + SplTokenMetadataProgram, +} from "../../utils"; + +export function createTransferWrappedInstruction( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + tokenId: bigint | number, + nonce: number, + targetAddress: Buffer | Uint8Array, + targetChain: number +): TransactionInstruction { + const methods = createReadOnlyNftBridgeProgramInterface( + nftBridgeProgramId + ).methods.transferWrapped( + nonce, + Buffer.from(targetAddress) as any, + targetChain + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferWrappedAccounts( + nftBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + fromOwner, + tokenChain, + tokenAddress, + tokenId + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferWrappedAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + fromOwner: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + splMetadata: PublicKey; + authoritySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + splMetadataProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getTransferWrappedAccounts( + nftBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + tokenId: bigint | number +): TransferWrappedAccounts { + const mint = deriveWrappedMintKey( + nftBridgeProgramId, + tokenChain, + tokenAddress, + tokenId + ); + const { + bridge: wormholeBridge, + message: wormholeMessage, + emitter: wormholeEmitter, + sequence: wormholeSequence, + feeCollector: wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageAccounts( + wormholeProgramId, + payer, + nftBridgeProgramId, + message + ); + return { + payer: new PublicKey(payer), + config: deriveNftBridgeConfigKey(nftBridgeProgramId), + from: new PublicKey(from), + fromOwner: new PublicKey(fromOwner), + mint, + wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint), + splMetadata: deriveSplTokenMetadataKey(mint), + authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId), + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + tokenProgram: TOKEN_PROGRAM_ID, + splMetadataProgram: SplTokenMetadataProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/nftBridge/program.ts b/sdk/js/src/solana/nftBridge/program.ts new file mode 100644 index 000000000..450519c97 --- /dev/null +++ b/sdk/js/src/solana/nftBridge/program.ts @@ -0,0 +1,43 @@ +import { Connection, PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { BN, Program, Provider } from "@project-serum/anchor"; +import { createReadOnlyProvider } from "../utils"; +import { NftBridgeCoder } from "./coder"; +import { NftBridge } from "../types/nftBridge"; + +import IDL from "../../anchor-idl/nft_bridge.json"; + +export const NFT_TRANSFER_NATIVE_TOKEN_ADDRESS = Buffer.alloc(32, 1); + +export function createNftBridgeProgramInterface( + programId: PublicKeyInitData, + provider?: Provider +): Program { + return new Program( + IDL as NftBridge, + new PublicKey(programId), + provider === undefined ? ({ connection: null } as any) : provider, + coder() + ); +} + +export function createReadOnlyNftBridgeProgramInterface( + programId: PublicKeyInitData, + connection?: Connection +): Program { + return createNftBridgeProgramInterface( + programId, + createReadOnlyProvider(connection) + ); +} + +export function coder(): NftBridgeCoder { + return new NftBridgeCoder(IDL as NftBridge); +} + +export function tokenIdToMint(tokenId: bigint) { + return new PublicKey(new BN(tokenId.toString()).toBuffer()); +} + +export function mintToTokenId(mint: PublicKeyInitData) { + return BigInt(new BN(new PublicKey(mint).toBuffer()).toString()); +} diff --git a/sdk/js/src/solana/postVaa.ts b/sdk/js/src/solana/postVaa.ts deleted file mode 100644 index 5a7b03088..000000000 --- a/sdk/js/src/solana/postVaa.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { - Connection, - Keypair, - PublicKey, - Transaction, - TransactionInstruction, -} from "@solana/web3.js"; -import { chunks } from ".."; -import { sendAndConfirmTransactionsWithRetry } from "../utils/solana"; -import { ixFromRust } from "./rust"; -import { importCoreWasm } from "./wasm"; - -export async function postVaaWithRetry( - connection: Connection, - signTransaction: (transaction: Transaction) => Promise, - bridge_id: string, - payer: string, - vaa: Buffer, - maxRetries: number -) { - const unsignedTransactions: Transaction[] = []; - const signature_set = Keypair.generate(); - const instructions = await createVerifySignaturesInstructions( - connection, - bridge_id, - payer, - vaa, - signature_set - ); - const finalInstruction = await createPostVaaInstruction( - bridge_id, - payer, - vaa, - signature_set - ); - if (!finalInstruction) { - return Promise.reject("Failed to construct the transaction."); - } - - //The verify signatures instructions can be batched into groups of 2 safely, - //reducing the total number of transactions. - const batchableChunks = chunks(instructions, 2); - batchableChunks.forEach((chunk) => { - let transaction; - if (chunk.length === 1) { - transaction = new Transaction().add(chunk[0]); - } else { - transaction = new Transaction().add(chunk[0], chunk[1]); - } - unsignedTransactions.push(transaction); - }); - - //the postVaa instruction can only execute after the verifySignature transactions have - //successfully completed. - const finalTransaction = new Transaction().add(finalInstruction); - - //The signature_set keypair also needs to sign the verifySignature transactions, thus a wrapper is needed. - const partialSignWrapper = (transaction: Transaction) => { - transaction.partialSign(signature_set); - return signTransaction(transaction); - }; - - await sendAndConfirmTransactionsWithRetry( - connection, - partialSignWrapper, - payer, - unsignedTransactions, - maxRetries - ); - //While the signature_set is used to create the final instruction, it doesn't need to sign it. - await sendAndConfirmTransactionsWithRetry( - connection, - signTransaction, - payer, - [finalTransaction], - maxRetries - ); - - return Promise.resolve(); -} - -/* -This returns an array of instructions required to verify the signatures of a VAA, and upload it to the blockchain. -signature_set should be a new keypair, and also needs to partial sign the transaction when these instructions are submitted. -*/ -export async function createVerifySignaturesInstructions( - connection: Connection, - bridge_id: string, - payer: string, - vaa: Buffer, - signature_set: Keypair -): Promise { - const output: TransactionInstruction[] = []; - const { - guardian_set_address, - parse_guardian_set, - parse_vaa, - verify_signatures_ix, - } = await importCoreWasm(); - const { guardian_set_index } = parse_vaa(new Uint8Array(vaa)); - let guardian_addr = new PublicKey( - guardian_set_address(bridge_id, guardian_set_index) - ); - let acc = await connection.getAccountInfo(guardian_addr); - if (acc?.data === undefined) { - return output; - } - let guardian_data = parse_guardian_set(new Uint8Array(acc?.data)); - - let txs = verify_signatures_ix( - bridge_id, - payer, - guardian_set_index, - guardian_data, - signature_set.publicKey.toString(), - vaa - ); - // Add transfer instruction to transaction - for (let tx of txs) { - let ixs: Array = tx.map((v: any) => { - return ixFromRust(v); - }); - output.push(ixs[0], ixs[1]); - } - return output; -} - -/* -This will return the postVaaInstruction. This should only be executed after the verifySignaturesInstructions have been executed. -signatureSetKeypair should be the same keypair used for verifySignaturesInstructions, but does not need to partialSign the transaction -when this instruction is submitted. -*/ -export async function createPostVaaInstruction( - bridge_id: string, - payer: string, - vaa: Buffer, - signatureSetKeypair: Keypair -): Promise { - const { post_vaa_ix } = await importCoreWasm(); - return ixFromRust( - post_vaa_ix(bridge_id, payer, signatureSetKeypair.publicKey.toString(), vaa) - ); -} - -/* - @deprecated - Instead, either use postVaaWithRetry or create, sign, and send the verifySignaturesInstructions & postVaaInstruction yourself. - - This function is equivalent to a postVaaWithRetry with a maxRetries of 0. -*/ -export async function postVaa( - connection: Connection, - signTransaction: (transaction: Transaction) => Promise, - bridge_id: string, - payer: string, - vaa: Buffer -) { - const { - guardian_set_address, - parse_guardian_set, - parse_vaa, - post_vaa_ix, - verify_signatures_ix, - } = await importCoreWasm(); - const { guardian_set_index } = parse_vaa(new Uint8Array(vaa)); - let guardian_addr = new PublicKey( - guardian_set_address(bridge_id, guardian_set_index) - ); - let acc = await connection.getAccountInfo(guardian_addr); - if (acc?.data === undefined) { - return; - } - let guardian_data = parse_guardian_set(new Uint8Array(acc?.data)); - - let signature_set = Keypair.generate(); - let txs = verify_signatures_ix( - bridge_id, - payer, - guardian_set_index, - guardian_data, - signature_set.publicKey.toString(), - vaa - ); - // Add transfer instruction to transaction - for (let tx of txs) { - let ixs: Array = tx.map((v: any) => { - return ixFromRust(v); - }); - let transaction = new Transaction().add(...ixs); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payer); - transaction.partialSign(signature_set); - - // Sign transaction, broadcast, and confirm - const signed = await signTransaction(transaction); - const txid = await connection.sendRawTransaction(signed.serialize()); - await connection.confirmTransaction(txid); - } - - let ix = ixFromRust( - post_vaa_ix(bridge_id, payer, signature_set.publicKey.toString(), vaa) - ); - let transaction = new Transaction().add(ix); - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payer); - - const signed = await signTransaction(transaction); - const txid = await connection.sendRawTransaction(signed.serialize()); - await connection.confirmTransaction(txid); -} diff --git a/sdk/js/src/solana/rust.ts b/sdk/js/src/solana/rust.ts deleted file mode 100644 index c08b256bb..000000000 --- a/sdk/js/src/solana/rust.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - AccountMeta, - PublicKey, - TransactionInstruction, -} from "@solana/web3.js"; -// begin from clients\solana\main.ts -export function ixFromRust(data: any): TransactionInstruction { - const keys: AccountMeta[] = data.accounts.map(accountMetaFromRust); - return new TransactionInstruction({ - programId: new PublicKey(data.program_id), - data: Buffer.from(data.data), - keys, - }); -} - -function accountMetaFromRust(meta: any): AccountMeta { - return { - pubkey: new PublicKey(meta.pubkey), - isSigner: meta.is_signer, - isWritable: meta.is_writable, - }; -} -// end from clients\solana\main.ts diff --git a/sdk/js/src/solana/sendAndConfirmPostVaa.ts b/sdk/js/src/solana/sendAndConfirmPostVaa.ts new file mode 100644 index 000000000..e6be2301a --- /dev/null +++ b/sdk/js/src/solana/sendAndConfirmPostVaa.ts @@ -0,0 +1,171 @@ +import { + Commitment, + ConfirmOptions, + Connection, + Keypair, + PublicKeyInitData, + Transaction, +} from "@solana/web3.js"; +import { + signSendAndConfirmTransaction, + SignTransaction, + sendAndConfirmTransactionsWithRetry, + modifySignTransaction, + TransactionSignatureAndResponse, + PreparedTransactions, +} from "./utils"; +import { + createPostVaaInstruction, + createVerifySignaturesInstructions, +} from "./wormhole"; +import { isBytes, ParsedVaa, parseVaa, SignedVaa } from "../vaa/wormhole"; + +export async function postVaaWithRetry( + connection: Connection, + signTransaction: SignTransaction, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: Buffer, + maxRetries?: number, + commitment?: Commitment +): Promise { + const { unsignedTransactions, signers } = + await createPostSignedVaaTransactions( + connection, + wormholeProgramId, + payer, + vaa, + commitment + ); + + const postVaaTransaction = unsignedTransactions.pop()!; + + const responses = await sendAndConfirmTransactionsWithRetry( + connection, + modifySignTransaction(signTransaction, ...signers), + payer.toString(), + unsignedTransactions, + maxRetries + ); + //While the signature_set is used to create the final instruction, it doesn't need to sign it. + responses.push( + ...(await sendAndConfirmTransactionsWithRetry( + connection, + signTransaction, + payer.toString(), + [postVaaTransaction], + maxRetries + )) + ); + return responses; +} + +export async function postVaa( + connection: Connection, + signTransaction: SignTransaction, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: Buffer, + options?: ConfirmOptions, + asyncVerifySignatures: boolean = true +): Promise { + const { unsignedTransactions, signers } = + await createPostSignedVaaTransactions( + connection, + wormholeProgramId, + payer, + vaa, + options?.commitment + ); + + const postVaaTransaction = unsignedTransactions.pop()!; + + const verifySignatures = async (transaction: Transaction) => + signSendAndConfirmTransaction( + connection, + payer, + modifySignTransaction(signTransaction, ...signers), + transaction, + options + ); + + const output: TransactionSignatureAndResponse[] = []; + if (asyncVerifySignatures) { + const verified = await Promise.all( + unsignedTransactions.map(async (transaction) => + verifySignatures(transaction) + ) + ); + output.push(...verified); + } else { + for (const transaction of unsignedTransactions) { + output.push(await verifySignatures(transaction)); + } + } + output.push( + await signSendAndConfirmTransaction( + connection, + payer, + signTransaction, + postVaaTransaction, + options + ) + ); + return output; +} + +/** Send transactions for `verify_signatures` and `post_vaa` instructions. + * + * Using a signed VAA, execute transactions generated by {@link verifySignatures} and + * {@link postVaa}. At most 4 transactions are sent (up to 3 from signature verification + * and 1 to post VAA data to an account). + * + * @param {Connection} connection - Solana web3 connection + * @param {PublicKeyInitData} wormholeProgramId - wormhole program address + * @param {web3.Keypair} payer - transaction signer address + * @param {Buffer} signedVaa - bytes of signed VAA + * @param {Commitment} [options] - Solana commitment + * + */ +export async function createPostSignedVaaTransactions( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa, + commitment?: Commitment +): Promise { + const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa; + const signatureSet = Keypair.generate(); + + const verifySignaturesInstructions = await createVerifySignaturesInstructions( + connection, + wormholeProgramId, + payer, + parsed, + signatureSet.publicKey, + commitment + ); + + const unsignedTransactions: Transaction[] = []; + for (let i = 0; i < verifySignaturesInstructions.length; i += 2) { + unsignedTransactions.push( + new Transaction().add(...verifySignaturesInstructions.slice(i, i + 2)) + ); + } + + unsignedTransactions.push( + new Transaction().add( + createPostVaaInstruction( + wormholeProgramId, + payer, + parsed, + signatureSet.publicKey + ) + ) + ); + + return { + unsignedTransactions, + signers: [signatureSet], + }; +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/config.ts b/sdk/js/src/solana/tokenBridge/accounts/config.ts new file mode 100644 index 000000000..df29ef9f3 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/config.ts @@ -0,0 +1,42 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { deriveAddress, getAccountData } from "../../utils"; + +export function deriveTokenBridgeConfigKey( + tokenBridgeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("config")], tokenBridgeProgramId); +} + +export async function getTokenBridgeConfig( + connection: Connection, + tokenBridgeProgramId: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo( + deriveTokenBridgeConfigKey(tokenBridgeProgramId), + commitment + ) + .then((info) => TokenBridgeConfig.deserialize(getAccountData(info))); +} + +export class TokenBridgeConfig { + wormhole: PublicKey; + + constructor(wormholeProgramId: Buffer) { + this.wormhole = new PublicKey(wormholeProgramId); + } + + static deserialize(data: Buffer): TokenBridgeConfig { + if (data.length != 32) { + throw new Error("data.length != 32"); + } + const wormholeProgramId = data.subarray(0, 32); + return new TokenBridgeConfig(wormholeProgramId); + } +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/custody.ts b/sdk/js/src/solana/tokenBridge/accounts/custody.ts new file mode 100644 index 000000000..8df6d7456 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/custody.ts @@ -0,0 +1,9 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export function deriveCustodyKey( + tokenBridgeProgramId: PublicKeyInitData, + mint: PublicKeyInitData +): PublicKey { + return deriveAddress([new PublicKey(mint).toBuffer()], tokenBridgeProgramId); +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/endpoint.ts b/sdk/js/src/solana/tokenBridge/accounts/endpoint.ts new file mode 100644 index 000000000..0cb66956f --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/endpoint.ts @@ -0,0 +1,70 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { + ChainId, + CHAIN_ID_SOLANA, + tryNativeToUint8Array, +} from "../../../utils"; +import { deriveAddress, getAccountData } from "../../utils"; + +export function deriveEndpointKey( + tokenBridgeProgramId: PublicKeyInitData, + emitterChain: number | ChainId, + emitterAddress: Buffer | Uint8Array | string +): PublicKey { + if (emitterChain == CHAIN_ID_SOLANA) { + throw new Error( + "emitterChain == CHAIN_ID_SOLANA cannot exist as foreign token bridge emitter" + ); + } + if (typeof emitterAddress == "string") { + emitterAddress = tryNativeToUint8Array( + emitterAddress, + emitterChain as ChainId + ); + } + return deriveAddress( + [ + (() => { + const buf = Buffer.alloc(2); + buf.writeUInt16BE(emitterChain as number); + return buf; + })(), + emitterAddress, + ], + tokenBridgeProgramId + ); +} + +export async function getEndpointRegistration( + connection: Connection, + endpointKey: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(new PublicKey(endpointKey), commitment) + .then((info) => EndpointRegistration.deserialize(getAccountData(info))); +} + +export class EndpointRegistration { + chain: ChainId; + contract: Buffer; + + constructor(chain: number, contract: Buffer) { + this.chain = chain as ChainId; + this.contract = contract; + } + + static deserialize(data: Buffer): EndpointRegistration { + if (data.length != 34) { + throw new Error("data.length != 34"); + } + const chain = data.readUInt16LE(0); + const contract = data.subarray(2, 34); + return new EndpointRegistration(chain, contract); + } +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/index.ts b/sdk/js/src/solana/tokenBridge/accounts/index.ts new file mode 100644 index 000000000..d501c6721 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/index.ts @@ -0,0 +1,7 @@ +export * from "./config"; +export * from "./custody"; +export * from "./endpoint"; +export * from "./transferWithPayload"; +export * from "./signer"; +export * from "./wrapped"; +export { deriveUpgradeAuthorityKey } from "../../wormhole"; diff --git a/sdk/js/src/solana/tokenBridge/accounts/signer.ts b/sdk/js/src/solana/tokenBridge/accounts/signer.ts new file mode 100644 index 000000000..e961b8203 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/signer.ts @@ -0,0 +1,20 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export function deriveAuthoritySignerKey( + tokenBridgeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("authority_signer")], tokenBridgeProgramId); +} + +export function deriveCustodySignerKey( + tokenBridgeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("custody_signer")], tokenBridgeProgramId); +} + +export function deriveMintAuthorityKey( + tokenBridgeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("mint_signer")], tokenBridgeProgramId); +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts b/sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts new file mode 100644 index 000000000..d7cea7d43 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/transferWithPayload.ts @@ -0,0 +1,14 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export function deriveSenderAccountKey( + cpiProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("sender")], cpiProgramId); +} + +export function deriveRedeemerAccountKey( + cpiProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("redeemer")], cpiProgramId); +} diff --git a/sdk/js/src/solana/tokenBridge/accounts/wrapped.ts b/sdk/js/src/solana/tokenBridge/accounts/wrapped.ts new file mode 100644 index 000000000..d302bdc96 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/accounts/wrapped.ts @@ -0,0 +1,87 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { + ChainId, + CHAIN_ID_SOLANA, + tryNativeToUint8Array, +} from "../../../utils"; +import { deriveAddress, getAccountData } from "../../utils"; + +export { deriveSplTokenMetadataKey } from "../../utils/splMetadata"; + +export function deriveWrappedMintKey( + tokenBridgeProgramId: PublicKeyInitData, + tokenChain: number | ChainId, + tokenAddress: Buffer | Uint8Array | string +): PublicKey { + if (tokenChain == CHAIN_ID_SOLANA) { + throw new Error( + "tokenChain == CHAIN_ID_SOLANA does not have wrapped mint key" + ); + } + if (typeof tokenAddress == "string") { + tokenAddress = tryNativeToUint8Array(tokenAddress, tokenChain as ChainId); + } + return deriveAddress( + [ + Buffer.from("wrapped"), + (() => { + const buf = Buffer.alloc(2); + buf.writeUInt16BE(tokenChain as number); + return buf; + })(), + tokenAddress, + ], + tokenBridgeProgramId + ); +} + +export function deriveWrappedMetaKey( + tokenBridgeProgramId: PublicKeyInitData, + mint: PublicKeyInitData +): PublicKey { + return deriveAddress( + [Buffer.from("meta"), new PublicKey(mint).toBuffer()], + tokenBridgeProgramId + ); +} + +export async function getWrappedMeta( + connection: Connection, + tokenBridgeProgramId: PublicKeyInitData, + mint: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo( + deriveWrappedMetaKey(tokenBridgeProgramId, mint), + commitment + ) + .then((info) => WrappedMeta.deserialize(getAccountData(info))); +} + +export class WrappedMeta { + chain: number; + tokenAddress: Buffer; + originalDecimals: number; + + constructor(chain: number, tokenAddress: Buffer, originalDecimals: number) { + this.chain = chain; + this.tokenAddress = tokenAddress; + this.originalDecimals = originalDecimals; + } + + static deserialize(data: Buffer): WrappedMeta { + if (data.length != 35) { + throw new Error("data.length != 35"); + } + const chain = data.readUInt16LE(0); + const tokenAddress = data.subarray(2, 34); + const originalDecimals = data.readUInt8(34); + return new WrappedMeta(chain, tokenAddress, originalDecimals); + } +} diff --git a/sdk/js/src/solana/tokenBridge/coder/accounts.ts b/sdk/js/src/solana/tokenBridge/coder/accounts.ts new file mode 100644 index 000000000..7ae89014d --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/accounts.ts @@ -0,0 +1,40 @@ +import { AccountsCoder, Idl } from "@project-serum/anchor"; +import { accountSize, IdlTypeDef } from "../../anchor"; + +export class TokenBridgeAccountsCoder + implements AccountsCoder +{ + constructor(private idl: Idl) {} + + public async encode(accountName: A, account: T): Promise { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public decode(accountName: A, ix: Buffer): T { + return this.decodeUnchecked(accountName, ix); + } + + public decodeUnchecked(accountName: A, ix: Buffer): T { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public memcmp(accountName: A, _appendData?: Buffer): any { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public size(idlAccount: IdlTypeDef): number { + return accountSize(this.idl, idlAccount) ?? 0; + } +} diff --git a/sdk/js/src/solana/tokenBridge/coder/events.ts b/sdk/js/src/solana/tokenBridge/coder/events.ts new file mode 100644 index 000000000..f4348ca9c --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/events.ts @@ -0,0 +1,12 @@ +import { EventCoder, Event, Idl } from "@project-serum/anchor"; +import { IdlEvent } from "../../anchor"; + +export class TokenBridgeEventsCoder implements EventCoder { + constructor(_idl: Idl) {} + + decode>( + _log: string + ): Event | null { + throw new Error("Token Bridge program does not have events"); + } +} diff --git a/sdk/js/src/solana/tokenBridge/coder/index.ts b/sdk/js/src/solana/tokenBridge/coder/index.ts new file mode 100644 index 000000000..d3a76a00e --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/index.ts @@ -0,0 +1,24 @@ +import { Coder, Idl } from "@project-serum/anchor"; +import { TokenBridgeAccountsCoder } from "./accounts"; +import { TokenBridgeEventsCoder } from "./events"; +import { TokenBridgeInstructionCoder } from "./instruction"; +import { TokenBridgeStateCoder } from "./state"; +import { TokenBridgeTypesCoder } from "./types"; + +export { TokenBridgeInstruction } from "./instruction"; + +export class TokenBridgeCoder implements Coder { + readonly instruction: TokenBridgeInstructionCoder; + readonly accounts: TokenBridgeAccountsCoder; + readonly state: TokenBridgeStateCoder; + readonly events: TokenBridgeEventsCoder; + readonly types: TokenBridgeTypesCoder; + + constructor(idl: Idl) { + this.instruction = new TokenBridgeInstructionCoder(idl); + this.accounts = new TokenBridgeAccountsCoder(idl); + this.state = new TokenBridgeStateCoder(idl); + this.events = new TokenBridgeEventsCoder(idl); + this.types = new TokenBridgeTypesCoder(idl); + } +} diff --git a/sdk/js/src/solana/tokenBridge/coder/instruction.ts b/sdk/js/src/solana/tokenBridge/coder/instruction.ts new file mode 100644 index 000000000..0d5d67fbe --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/instruction.ts @@ -0,0 +1,254 @@ +import { Idl, InstructionCoder } from "@project-serum/anchor"; +import { PublicKey } from "@solana/web3.js"; + +export class TokenBridgeInstructionCoder implements InstructionCoder { + constructor(_: Idl) {} + + encode(ixName: string, ix: any): Buffer { + switch (ixName) { + case "initialize": { + return encodeInitialize(ix); + } + case "attestToken": { + return encodeAttestToken(ix); + } + case "completeNative": { + return encodeCompleteNative(ix); + } + case "completeWrapped": { + return encodeCompleteWrapped(ix); + } + case "transferWrapped": { + return encodeTransferWrapped(ix); + } + case "transferNative": { + return encodeTransferNative(ix); + } + case "registerChain": { + return encodeRegisterChain(ix); + } + case "createWrapped": { + return encodeCreateWrapped(ix); + } + case "upgradeContract": { + return encodeUpgradeContract(ix); + } + case "transferWrappedWithPayload": { + return encodeTransferWrappedWithPayload(ix); + } + case "transferNativeWithPayload": { + return encodeTransferNativeWithPayload(ix); + } + default: { + throw new Error(`Invalid instruction: ${ixName}`); + } + } + } + + encodeState(_ixName: string, _ix: any): Buffer { + throw new Error("Token Bridge program does not have state"); + } +} + +/** Solitaire enum of existing the Token Bridge's instructions. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/modules/token_bridge/program/src/lib.rs#L100 + */ +export enum TokenBridgeInstruction { + Initialize, + AttestToken, + CompleteNative, + CompleteWrapped, + TransferWrapped, + TransferNative, + RegisterChain, + CreateWrapped, + UpgradeContract, + CompleteNativeWithPayload, + CompleteWrappedWithPayload, + TransferWrappedWithPayload, + TransferNativeWithPayload, +} + +function encodeTokenBridgeInstructionData( + instructionType: TokenBridgeInstruction, + data?: Buffer +): Buffer { + const dataLen = data === undefined ? 0 : data.length; + const instructionData = Buffer.alloc(1 + dataLen); + instructionData.writeUInt8(instructionType, 0); + if (dataLen > 0) { + instructionData.write(data!.toString("hex"), 1, "hex"); + } + return instructionData; +} + +function encodeInitialize({ wormhole }: any): Buffer { + const serialized = Buffer.alloc(32); + serialized.write( + new PublicKey(wormhole).toBuffer().toString("hex"), + 0, + "hex" + ); + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.Initialize, + serialized + ); +} + +function encodeAttestToken({ nonce }: any) { + const serialized = Buffer.alloc(4); + serialized.writeUInt32LE(nonce, 0); + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.AttestToken, + serialized + ); +} + +function encodeCompleteNative({}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.CompleteNative + ); +} + +function encodeCompleteWrapped({}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.CompleteWrapped + ); +} + +function encodeTransferData({ + nonce, + amount, + fee, + targetAddress, + targetChain, +}: any) { + if (typeof amount != "bigint") { + amount = BigInt(amount); + } + if (typeof fee != "bigint") { + fee = BigInt(fee); + } + if (!Buffer.isBuffer(targetAddress)) { + throw new Error("targetAddress must be Buffer"); + } + const serialized = Buffer.alloc(54); + serialized.writeUInt32LE(nonce, 0); + serialized.writeBigUInt64LE(amount, 4); + serialized.writeBigUInt64LE(fee, 12); + serialized.write(targetAddress.toString("hex"), 20, "hex"); + serialized.writeUInt16LE(targetChain, 52); + return serialized; +} + +function encodeTransferWrapped({ + nonce, + amount, + fee, + targetAddress, + targetChain, +}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.TransferWrapped, + encodeTransferData({ nonce, amount, fee, targetAddress, targetChain }) + ); +} + +function encodeTransferNative({ + nonce, + amount, + fee, + targetAddress, + targetChain, +}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.TransferNative, + encodeTransferData({ nonce, amount, fee, targetAddress, targetChain }) + ); +} + +function encodeRegisterChain({}: any) { + return encodeTokenBridgeInstructionData(TokenBridgeInstruction.RegisterChain); +} + +function encodeCreateWrapped({}: any) { + return encodeTokenBridgeInstructionData(TokenBridgeInstruction.CreateWrapped); +} + +function encodeUpgradeContract({}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.UpgradeContract + ); +} + +function encodeTransferWithPayloadData({ + nonce, + amount, + targetAddress, + targetChain, + payload, +}: any) { + if (typeof amount != "bigint") { + amount = BigInt(amount); + } + if (!Buffer.isBuffer(targetAddress)) { + throw new Error("targetAddress must be Buffer"); + } + if (!Buffer.isBuffer(payload)) { + throw new Error("payload must be Buffer"); + } + const serializedWithPayloadLen = Buffer.alloc(50); + serializedWithPayloadLen.writeUInt32LE(nonce, 0); + serializedWithPayloadLen.writeBigUInt64LE(amount, 4); + serializedWithPayloadLen.write(targetAddress.toString("hex"), 12, "hex"); + serializedWithPayloadLen.writeUInt16LE(targetChain, 44); + serializedWithPayloadLen.writeUInt32LE(payload.length, 46); + return Buffer.concat([ + serializedWithPayloadLen, + payload, + Buffer.alloc(1), // option == None + ]); +} + +function encodeTransferWrappedWithPayload({ + nonce, + amount, + fee, + targetAddress, + targetChain, + payload, +}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.TransferWrappedWithPayload, + encodeTransferWithPayloadData({ + nonce, + amount, + fee, + targetAddress, + targetChain, + payload, + }) + ); +} + +function encodeTransferNativeWithPayload({ + nonce, + amount, + fee, + targetAddress, + targetChain, + payload, +}: any) { + return encodeTokenBridgeInstructionData( + TokenBridgeInstruction.TransferNativeWithPayload, + encodeTransferWithPayloadData({ + nonce, + amount, + fee, + targetAddress, + targetChain, + payload, + }) + ); +} diff --git a/sdk/js/src/solana/tokenBridge/coder/state.ts b/sdk/js/src/solana/tokenBridge/coder/state.ts new file mode 100644 index 000000000..c6d68af2f --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/state.ts @@ -0,0 +1,12 @@ +import { Idl, StateCoder } from "@project-serum/anchor"; + +export class TokenBridgeStateCoder implements StateCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _account: T): Promise { + throw new Error("Token Bridge program does not have state"); + } + decode(_ix: Buffer): T { + throw new Error("Token Bridge program does not have state"); + } +} diff --git a/sdk/js/src/solana/tokenBridge/coder/types.ts b/sdk/js/src/solana/tokenBridge/coder/types.ts new file mode 100644 index 000000000..219957d2b --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/coder/types.ts @@ -0,0 +1,12 @@ +import { Idl, TypesCoder } from "@project-serum/anchor"; + +export class TokenBridgeTypesCoder implements TypesCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _type: T): Buffer { + throw new Error("Token Bridge program does not have user-defined types"); + } + decode(_name: string, _typeData: Buffer): T { + throw new Error("Token Bridge program does not have user-defined types"); + } +} diff --git a/sdk/js/src/solana/tokenBridge/cpi.ts b/sdk/js/src/solana/tokenBridge/cpi.ts new file mode 100644 index 000000000..d9164cd8b --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/cpi.ts @@ -0,0 +1,477 @@ +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, +} from "@solana/web3.js"; +import { + isBytes, + ParsedTokenTransferVaa, + parseTokenTransferVaa, + SignedVaa, +} from "../../vaa"; +import { + deriveClaimKey, + derivePostedVaaKey, + getWormholeDerivedAccounts, +} from "../wormhole"; +import { + deriveAuthoritySignerKey, + deriveCustodyKey, + deriveCustodySignerKey, + deriveEndpointKey, + deriveMintAuthorityKey, + deriveRedeemerAccountKey, + deriveSenderAccountKey, + deriveTokenBridgeConfigKey, + deriveWrappedMetaKey, + deriveWrappedMintKey, +} from "./accounts"; +import { + getTransferNativeWithPayloadAccounts, + getTransferWrappedWithPayloadAccounts, +} from "./instructions"; + +export interface TokenBridgeBaseDerivedAccounts { + /** + * seeds = ["config"], seeds::program = tokenBridgeProgram + */ + tokenBridgeConfig: PublicKey; +} + +export interface TokenBridgeBaseNativeDerivedAccounts + extends TokenBridgeBaseDerivedAccounts { + /** + * seeds = ["custody_signer"], seeds::program = tokenBridgeProgram + */ + tokenBridgeCustodySigner: PublicKey; +} + +export interface TokenBridgeBaseSenderDerivedAccounts + extends TokenBridgeBaseDerivedAccounts { + /** + * seeds = ["authority_signer"], seeds::program = tokenBridgeProgram + */ + tokenBridgeAuthoritySigner: PublicKey; + /** + * seeds = ["sender"], seeds::program = cpiProgramId + */ + tokenBridgeSender: PublicKey; + /** + * seeds = ["Bridge"], seeds::program = wormholeProgram + */ + wormholeBridge: PublicKey; + /** + * seeds = ["emitter"], seeds::program = tokenBridgeProgram + */ + tokenBridgeEmitter: PublicKey; + /** + * seeds = ["Sequence", tokenBridgeEmitter], seeds::program = wormholeProgram + */ + tokenBridgeSequence: PublicKey; + /** + * seeds = ["fee_collector"], seeds::program = wormholeProgram + */ + wormholeFeeCollector: PublicKey; +} + +export interface TokenBridgeNativeSenderDerivedAccounts + extends TokenBridgeBaseNativeDerivedAccounts, + TokenBridgeBaseSenderDerivedAccounts {} + +export interface TokenBridgeWrappedSenderDerivedAccounts + extends TokenBridgeBaseSenderDerivedAccounts {} + +export interface TokenBridgeBaseRedeemerDerivedAccounts + extends TokenBridgeBaseDerivedAccounts { + /** + * seeds = ["redeemer"], seeds::program = cpiProgramId + */ + tokenBridgeRedeemer: PublicKey; +} + +export interface TokenBridgeNativeRedeemerDerivedAccounts + extends TokenBridgeBaseNativeDerivedAccounts, + TokenBridgeBaseRedeemerDerivedAccounts {} + +export interface TokenBridgeWrappedRedeemerDerivedAccounts + extends TokenBridgeBaseRedeemerDerivedAccounts { + /** + * seeds = ["mint_signer"], seeds::program = tokenBridgeProgram + */ + tokenBridgeMintAuthority: PublicKey; +} + +export interface TokenBridgeDerivedAccounts + extends TokenBridgeNativeSenderDerivedAccounts, + TokenBridgeWrappedSenderDerivedAccounts, + TokenBridgeNativeRedeemerDerivedAccounts, + TokenBridgeWrappedRedeemerDerivedAccounts {} + +/** + * Generate Token Bridge PDAs. + * + * @param cpiProgramId + * @param tokenBridgeProgramId + * @param wormholeProgramId + * @returns + */ +export function getTokenBridgeDerivedAccounts( + cpiProgramId: PublicKeyInitData, + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): TokenBridgeDerivedAccounts { + const { + wormholeEmitter: tokenBridgeEmitter, + wormholeBridge, + wormholeFeeCollector, + wormholeSequence: tokenBridgeSequence, + } = getWormholeDerivedAccounts(tokenBridgeProgramId, wormholeProgramId); + return { + tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + tokenBridgeAuthoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId), + tokenBridgeCustodySigner: deriveCustodySignerKey(tokenBridgeProgramId), + tokenBridgeMintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId), + tokenBridgeSender: deriveSenderAccountKey(cpiProgramId), + tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId), + wormholeBridge, + tokenBridgeEmitter, + wormholeFeeCollector, + tokenBridgeSequence, + }; +} + +export interface TransferNativeWithPayloadCpiAccounts + extends TokenBridgeNativeSenderDerivedAccounts { + payer: PublicKey; + /** + * seeds = [mint], seeds::program = tokenBridgeProgram + */ + tokenBridgeCustody: PublicKey; + /** + * Token account where tokens reside + */ + fromTokenAccount: PublicKey; + mint: PublicKey; + wormholeMessage: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +/** + * Generate accounts needed to perform `transfer_wrapped_with_payload` instruction + * as cross-program invocation. + * + * @param cpiProgramId + * @param tokenBridgeProgramId + * @param wormholeProgramId + * @param payer + * @param message + * @param fromTokenAccount + * @param mint + * @returns + */ +export function getTransferNativeWithPayloadCpiAccounts( + cpiProgramId: PublicKeyInitData, + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + fromTokenAccount: PublicKeyInitData, + mint: PublicKeyInitData +): TransferNativeWithPayloadCpiAccounts { + const accounts = getTransferNativeWithPayloadAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + fromTokenAccount, + mint, + cpiProgramId + ); + return { + payer: accounts.payer, + tokenBridgeConfig: accounts.config, + fromTokenAccount: accounts.from, + mint: accounts.mint, + tokenBridgeCustody: accounts.custody, + tokenBridgeAuthoritySigner: accounts.authoritySigner, + tokenBridgeCustodySigner: accounts.custodySigner, + wormholeBridge: accounts.wormholeBridge, + wormholeMessage: accounts.wormholeMessage, + tokenBridgeEmitter: accounts.wormholeEmitter, + tokenBridgeSequence: accounts.wormholeSequence, + wormholeFeeCollector: accounts.wormholeFeeCollector, + clock: accounts.clock, + tokenBridgeSender: accounts.sender, + rent: accounts.rent, + systemProgram: accounts.systemProgram, + tokenProgram: accounts.tokenProgram, + wormholeProgram: accounts.wormholeProgram, + }; +} + +export interface TransferWrappedWithPayloadCpiAccounts + extends TokenBridgeWrappedSenderDerivedAccounts { + payer: PublicKey; + /** + * Token account where tokens reside + */ + fromTokenAccount: PublicKey; + /** + * Token account owner (usually cpiProgramId) + */ + fromTokenAccountOwner: PublicKey; + /** + * seeds = ["wrapped", token_chain, token_address], seeds::program = tokenBridgeProgram + */ + tokenBridgeWrappedMint: PublicKey; + /** + * seeds = ["meta", mint], seeds::program = tokenBridgeProgram + */ + tokenBridgeWrappedMeta: PublicKey; + wormholeMessage: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +/** + * Generate accounts needed to perform `transfer_wrapped_with_payload` instruction + * as cross-program invocation. + * + * @param cpiProgramId + * @param tokenBridgeProgramId + * @param wormholeProgramId + * @param payer + * @param message + * @param fromTokenAccount + * @param tokenChain + * @param tokenAddress + * @param [fromTokenAccountOwner] + * @returns + */ +export function getTransferWrappedWithPayloadCpiAccounts( + cpiProgramId: PublicKeyInitData, + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + fromTokenAccount: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + fromTokenAccountOwner?: PublicKeyInitData +): TransferWrappedWithPayloadCpiAccounts { + const accounts = getTransferWrappedWithPayloadAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + fromTokenAccount, + fromTokenAccountOwner === undefined ? cpiProgramId : fromTokenAccountOwner, + tokenChain, + tokenAddress, + cpiProgramId + ); + return { + payer: accounts.payer, + tokenBridgeConfig: accounts.config, + fromTokenAccount: accounts.from, + fromTokenAccountOwner: accounts.fromOwner, + tokenBridgeWrappedMint: accounts.mint, + tokenBridgeWrappedMeta: accounts.wrappedMeta, + tokenBridgeAuthoritySigner: accounts.authoritySigner, + wormholeBridge: accounts.wormholeBridge, + wormholeMessage: accounts.wormholeMessage, + tokenBridgeEmitter: accounts.wormholeEmitter, + tokenBridgeSequence: accounts.wormholeSequence, + wormholeFeeCollector: accounts.wormholeFeeCollector, + clock: accounts.clock, + tokenBridgeSender: accounts.sender, + rent: accounts.rent, + systemProgram: accounts.systemProgram, + tokenProgram: accounts.tokenProgram, + wormholeProgram: accounts.wormholeProgram, + }; +} + +export interface CompleteTransferNativeWithPayloadCpiAccounts + extends TokenBridgeNativeRedeemerDerivedAccounts { + payer: PublicKey; + /** + * seeds = ["PostedVAA", vaa_hash], seeds::program = wormholeProgram + */ + vaa: PublicKey; + /** + * seeds = [emitter_address, emitter_chain, sequence], seeds::program = tokenBridgeProgram + */ + tokenBridgeClaim: PublicKey; + /** + * seeds = [emitter_chain, emitter_address], seeds::program = tokenBridgeProgram + */ + tokenBridgeForeignEndpoint: PublicKey; + /** + * Token account to receive tokens + */ + toTokenAccount: PublicKey; + toFeesTokenAccount: PublicKey; // this shouldn't exist? + /** + * seeds = [mint], seeds::program = tokenBridgeProgram + */ + tokenBridgeCustody: PublicKey; + mint: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +/** + * Generate accounts needed to perform `complete_native_with_payload` instruction + * as cross-program invocation. + * + * Note: `toFeesTokenAccount` is the same as `toTokenAccount`. For your program, + * you only need to pass your `toTokenAccount` into the complete transfer + * instruction for the `toFeesTokenAccount`. + * + * @param cpiProgramId + * @param tokenBridgeProgramId + * @param wormholeProgramId + * @param payer + * @param vaa + * @returns + */ +export function getCompleteTransferNativeWithPayloadCpiAccounts( + cpiProgramId: PublicKeyInitData, + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa +): CompleteTransferNativeWithPayloadCpiAccounts { + const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa; + const mint = new PublicKey(parsed.tokenAddress); + const toTokenAccount = new PublicKey(parsed.to); + return { + payer: new PublicKey(payer), + tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + tokenBridgeClaim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + tokenBridgeForeignEndpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + toTokenAccount, + tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId), + toFeesTokenAccount: toTokenAccount, + tokenBridgeCustody: deriveCustodyKey(tokenBridgeProgramId, mint), + mint, + tokenBridgeCustodySigner: deriveCustodySignerKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} + +export interface CompleteTransferWrappedWithPayloadCpiAccounts + extends TokenBridgeWrappedRedeemerDerivedAccounts { + payer: PublicKey; + /** + * seeds = ["PostedVAA", vaa_hash], seeds::program = wormholeProgram + */ + vaa: PublicKey; + /** + * seeds = [emitter_address, emitter_chain, sequence], seeds::program = tokenBridgeProgram + */ + tokenBridgeClaim: PublicKey; + /** + * seeds = [emitter_chain, emitter_address], seeds::program = tokenBridgeProgram + */ + tokenBridgeForeignEndpoint: PublicKey; + /** + * Token account to receive tokens + */ + toTokenAccount: PublicKey; + toFeesTokenAccount: PublicKey; // this shouldn't exist? + /** + * seeds = ["wrapped", token_chain, token_address], seeds::program = tokenBridgeProgram + */ + tokenBridgeWrappedMint: PublicKey; + /** + * seeds = ["meta", mint], seeds::program = tokenBridgeProgram + */ + tokenBridgeWrappedMeta: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +/** + * Generate accounts needed to perform `complete_wrapped_with_payload` instruction + * as cross-program invocation. + * + * Note: `toFeesTokenAccount` is the same as `toTokenAccount`. For your program, + * you only need to pass your `toTokenAccount` into the complete transfer + * instruction for the `toFeesTokenAccount`. + * + * @param cpiProgramId + * @param tokenBridgeProgramId + * @param wormholeProgramId + * @param payer + * @param vaa + * @returns + */ +export function getCompleteTransferWrappedWithPayloadCpiAccounts( + cpiProgramId: PublicKeyInitData, + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa +): CompleteTransferWrappedWithPayloadCpiAccounts { + const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa; + const mint = deriveWrappedMintKey( + tokenBridgeProgramId, + parsed.tokenChain, + parsed.tokenAddress + ); + const toTokenAccount = new PublicKey(parsed.to); + return { + payer: new PublicKey(payer), + tokenBridgeConfig: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + tokenBridgeClaim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + tokenBridgeForeignEndpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + toTokenAccount, + tokenBridgeRedeemer: deriveRedeemerAccountKey(cpiProgramId), + toFeesTokenAccount: toTokenAccount, + tokenBridgeWrappedMint: mint, + tokenBridgeWrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + tokenBridgeMintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/index.ts b/sdk/js/src/solana/tokenBridge/index.ts new file mode 100644 index 000000000..81253083e --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/index.ts @@ -0,0 +1,4 @@ +export * from "./accounts"; +export * from "./cpi"; +export * from "./instructions"; +export * from "./program"; diff --git a/sdk/js/src/solana/tokenBridge/instructions/approve.ts b/sdk/js/src/solana/tokenBridge/instructions/approve.ts new file mode 100644 index 000000000..0f2385cd2 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/approve.ts @@ -0,0 +1,17 @@ +import { createApproveInstruction } from "@solana/spl-token"; +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAuthoritySignerKey } from "../accounts"; + +export function createApproveAuthoritySignerInstruction( + tokenBridgeProgramId: PublicKeyInitData, + tokenAccount: PublicKeyInitData, + owner: PublicKeyInitData, + amount: number | bigint +) { + return createApproveInstruction( + new PublicKey(tokenAccount), + deriveAuthoritySignerKey(tokenBridgeProgramId), + new PublicKey(owner), + amount + ); +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/attestToken.ts b/sdk/js/src/solana/tokenBridge/instructions/attestToken.ts new file mode 100644 index 000000000..e75d71266 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/attestToken.ts @@ -0,0 +1,97 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { getPostMessageAccounts } from "../../wormhole"; +import { + deriveSplTokenMetadataKey, + deriveTokenBridgeConfigKey, + deriveWrappedMetaKey, +} from "../accounts"; + +export function createAttestTokenInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + mint: PublicKeyInitData, + message: PublicKeyInitData, + nonce: number +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.attestToken(nonce); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getAttestTokenAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + mint, + message + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface AttestTokenAccounts { + payer: PublicKey; + config: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + splMetadata: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getAttestTokenAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + mint: PublicKeyInitData, + message: PublicKeyInitData +): AttestTokenAccounts { + const { + bridge: wormholeBridge, + emitter: wormholeEmitter, + sequence: wormholeSequence, + feeCollector: wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageAccounts( + wormholeProgramId, + payer, + tokenBridgeProgramId, + message + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + mint: new PublicKey(mint), + wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + splMetadata: deriveSplTokenMetadataKey(mint), + wormholeBridge, + wormholeMessage: new PublicKey(message), + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/completeNative.ts b/sdk/js/src/solana/tokenBridge/instructions/completeNative.ts new file mode 100644 index 000000000..36f93c683 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/completeNative.ts @@ -0,0 +1,105 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveTokenBridgeConfigKey, + deriveCustodyKey, + deriveCustodySignerKey, +} from "../accounts"; +import { + isBytes, + ParsedTokenTransferVaa, + parseTokenTransferVaa, + SignedVaa, +} from "../../../vaa"; + +export function createCompleteTransferNativeInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa, + feeRecipient?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.completeNative(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCompleteTransferNativeAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + vaa, + feeRecipient + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CompleteTransferNativeAccounts { + payer: PublicKey; + config: PublicKey; + vaa: PublicKey; + claim: PublicKey; + endpoint: PublicKey; + to: PublicKey; + toFees: PublicKey; + custody: PublicKey; + mint: PublicKey; + custodySigner: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCompleteTransferNativeAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa, + feeRecipient?: PublicKeyInitData +): CompleteTransferNativeAccounts { + const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa; + const mint = new PublicKey(parsed.tokenAddress); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + endpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + to: new PublicKey(parsed.to), + toFees: new PublicKey( + feeRecipient === undefined ? parsed.to : feeRecipient + ), + custody: deriveCustodyKey(tokenBridgeProgramId, mint), + mint, + custodySigner: deriveCustodySignerKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts b/sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts new file mode 100644 index 000000000..d3a6bf15c --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/completeWrapped.ts @@ -0,0 +1,110 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveTokenBridgeConfigKey, + deriveWrappedMintKey, + deriveWrappedMetaKey, + deriveMintAuthorityKey, +} from "../accounts"; +import { + isBytes, + ParsedTokenTransferVaa, + parseTokenTransferVaa, + SignedVaa, +} from "../../../vaa"; + +export function createCompleteTransferWrappedInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa, + feeRecipient?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.completeWrapped(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCompleteTransferWrappedAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + vaa, + feeRecipient + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CompleteTransferWrappedAccounts { + payer: PublicKey; + config: PublicKey; + vaa: PublicKey; + claim: PublicKey; + endpoint: PublicKey; + to: PublicKey; + toFees: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + mintAuthority: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCompleteTransferWrappedAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenTransferVaa, + feeRecipient?: PublicKeyInitData +): CompleteTransferWrappedAccounts { + const parsed = isBytes(vaa) ? parseTokenTransferVaa(vaa) : vaa; + const mint = deriveWrappedMintKey( + tokenBridgeProgramId, + parsed.tokenChain, + parsed.tokenAddress + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + endpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + to: new PublicKey(parsed.to), + toFees: new PublicKey( + feeRecipient === undefined ? parsed.to : feeRecipient + ), + mint, + wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts b/sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts new file mode 100644 index 000000000..d9228ddf5 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/createWrapped.ts @@ -0,0 +1,107 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveMintAuthorityKey, + deriveSplTokenMetadataKey, + deriveWrappedMetaKey, + deriveTokenBridgeConfigKey, + deriveWrappedMintKey, +} from "../accounts"; +import { + isBytes, + parseAttestMetaVaa, + ParsedAttestMetaVaa, + SignedVaa, +} from "../../../vaa"; +import { SplTokenMetadataProgram } from "../../utils"; + +export function createCreateWrappedInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedAttestMetaVaa +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.createWrapped(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getCreateWrappedAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface CreateWrappedAccounts { + payer: PublicKey; + config: PublicKey; + endpoint: PublicKey; + vaa: PublicKey; + claim: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + splMetadata: PublicKey; + mintAuthority: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + splMetadataProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getCreateWrappedAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedAttestMetaVaa +): CreateWrappedAccounts { + const parsed = isBytes(vaa) ? parseAttestMetaVaa(vaa) : vaa; + const mint = deriveWrappedMintKey( + tokenBridgeProgramId, + parsed.tokenChain, + parsed.tokenAddress + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + endpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.emitterChain, + parsed.emitterAddress + ), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + mint, + wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + splMetadata: deriveSplTokenMetadataKey(mint), + mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + splMetadataProgram: SplTokenMetadataProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/governance.ts b/sdk/js/src/solana/tokenBridge/instructions/governance.ts new file mode 100644 index 000000000..659f9a248 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/governance.ts @@ -0,0 +1,161 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole"; +import { + deriveEndpointKey, + deriveTokenBridgeConfigKey, + deriveUpgradeAuthorityKey, +} from "../accounts"; +import { + isBytes, + ParsedTokenBridgeRegisterChainVaa, + ParsedTokenBridgeUpgradeContractVaa, + parseTokenBridgeRegisterChainVaa, + parseTokenBridgeUpgradeContractVaa, + SignedVaa, +} from "../../../vaa"; +import { BpfLoaderUpgradeable, deriveUpgradeableProgramKey } from "../../utils"; + +export function createRegisterChainInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenBridgeRegisterChainVaa +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.registerChain(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getRegisterChainAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface RegisterChainAccounts { + payer: PublicKey; + config: PublicKey; + endpoint: PublicKey; + vaa: PublicKey; + claim: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getRegisterChainAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenBridgeRegisterChainVaa +): RegisterChainAccounts { + const parsed = isBytes(vaa) ? parseTokenBridgeRegisterChainVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + endpoint: deriveEndpointKey( + tokenBridgeProgramId, + parsed.foreignChain, + parsed.foreignAddress + ), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} + +export function createUpgradeContractInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenBridgeUpgradeContractVaa, + spill?: PublicKeyInitData +): TransactionInstruction { + const methods = + createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.upgradeContract(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getUpgradeContractAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + vaa, + spill + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface UpgradeContractAccounts { + payer: PublicKey; + vaa: PublicKey; + claim: PublicKey; + upgradeAuthority: PublicKey; + spill: PublicKey; + implementation: PublicKey; + programData: PublicKey; + tokenBridgeProgram: PublicKey; + rent: PublicKey; + clock: PublicKey; + bpfLoaderUpgradeable: PublicKey; + systemProgram: PublicKey; +} + +export function getUpgradeContractAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedTokenBridgeUpgradeContractVaa, + spill?: PublicKeyInitData +): UpgradeContractAccounts { + const parsed = isBytes(vaa) ? parseTokenBridgeUpgradeContractVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + tokenBridgeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + upgradeAuthority: deriveUpgradeAuthorityKey(tokenBridgeProgramId), + spill: new PublicKey(spill === undefined ? payer : spill), + implementation: new PublicKey(parsed.newContract), + programData: deriveUpgradeableProgramKey(tokenBridgeProgramId), + tokenBridgeProgram: new PublicKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + bpfLoaderUpgradeable: BpfLoaderUpgradeable.programId, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/index.ts b/sdk/js/src/solana/tokenBridge/instructions/index.ts new file mode 100644 index 000000000..e5e564aec --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/index.ts @@ -0,0 +1,11 @@ +export * from "./approve"; +export * from "./attestToken"; +export * from "./completeNative"; +export * from "./completeWrapped"; +export * from "./createWrapped"; +export * from "./initialize"; +export * from "./governance"; +export * from "./transferNative"; +export * from "./transferNativeWithPayload"; +export * from "./transferWrapped"; +export * from "./transferWrappedWithPayload"; diff --git a/sdk/js/src/solana/tokenBridge/instructions/initialize.ts b/sdk/js/src/solana/tokenBridge/instructions/initialize.ts new file mode 100644 index 000000000..a019c4088 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/initialize.ts @@ -0,0 +1,47 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { deriveTokenBridgeConfigKey } from "../accounts"; + +export function createInitializeInstruction( + tokenBridgeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): TransactionInstruction { + const methods = createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.initialize(wormholeProgramId as any); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getInitializeAccounts(tokenBridgeProgramId, payer) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface InitializeAccounts { + payer: PublicKey; + config: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getInitializeAccounts( + tokenBridgeProgramId: PublicKeyInitData, + payer: PublicKeyInitData +): InitializeAccounts { + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/transferNative.ts b/sdk/js/src/solana/tokenBridge/instructions/transferNative.ts new file mode 100644 index 000000000..accdaeb3f --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/transferNative.ts @@ -0,0 +1,118 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { getPostMessageCpiAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveCustodySignerKey, + deriveTokenBridgeConfigKey, + deriveCustodyKey, +} from "../accounts"; + +export function createTransferNativeInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData, + nonce: number, + amount: bigint, + fee: bigint, + targetAddress: Buffer | Uint8Array, + targetChain: number +): TransactionInstruction { + const methods = createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.transferNative( + nonce, + amount as any, + fee as any, + Buffer.from(targetAddress) as any, + targetChain + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferNativeAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + mint + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferNativeAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + mint: PublicKey; + custody: PublicKey; + authoritySigner: PublicKey; + custodySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getTransferNativeAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData +): TransferNativeAccounts { + const { + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageCpiAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + from: new PublicKey(from), + mint: new PublicKey(mint), + custody: deriveCustodyKey(tokenBridgeProgramId, mint), + authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId), + custodySigner: deriveCustodySignerKey(tokenBridgeProgramId), + wormholeBridge, + wormholeMessage: wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts b/sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts new file mode 100644 index 000000000..cb8f7f3c7 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/transferNativeWithPayload.ts @@ -0,0 +1,125 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { getPostMessageCpiAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveCustodySignerKey, + deriveTokenBridgeConfigKey, + deriveCustodyKey, + deriveSenderAccountKey, +} from "../accounts"; + +export function createTransferNativeWithPayloadInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData, + nonce: number, + amount: bigint, + targetAddress: Buffer | Uint8Array, + targetChain: number, + payload: Buffer | Uint8Array +): TransactionInstruction { + const methods = createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.transferNativeWithPayload( + nonce, + amount as any, + Buffer.from(targetAddress) as any, + targetChain, + Buffer.from(payload) as any, + null + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferNativeWithPayloadAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + mint + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferNativeWithPayloadAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + mint: PublicKey; + custody: PublicKey; + authoritySigner: PublicKey; + custodySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + sender: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getTransferNativeWithPayloadAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + mint: PublicKeyInitData, + cpiProgramId?: PublicKeyInitData +): TransferNativeWithPayloadAccounts { + const { + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageCpiAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + from: new PublicKey(from), + mint: new PublicKey(mint), + custody: deriveCustodyKey(tokenBridgeProgramId, mint), + authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId), + custodySigner: deriveCustodySignerKey(tokenBridgeProgramId), + wormholeBridge, + wormholeMessage: wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + sender: new PublicKey( + cpiProgramId === undefined ? payer : deriveSenderAccountKey(cpiProgramId) + ), + rent, + systemProgram, + tokenProgram: TOKEN_PROGRAM_ID, + wormholeProgram: new PublicKey(wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts b/sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts new file mode 100644 index 000000000..bf28a6566 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/transferWrapped.ts @@ -0,0 +1,129 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { getPostMessageCpiAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveTokenBridgeConfigKey, + deriveWrappedMetaKey, + deriveWrappedMintKey, +} from "../accounts"; + +export function createTransferWrappedInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + nonce: number, + amount: bigint, + fee: bigint, + targetAddress: Buffer | Uint8Array, + targetChain: number +): TransactionInstruction { + const methods = createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.transferWrapped( + nonce, + amount as any, + fee as any, + Buffer.from(targetAddress) as any, + targetChain + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferWrappedAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + fromOwner, + tokenChain, + tokenAddress + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferWrappedAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + fromOwner: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + authoritySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + wormholeProgram: PublicKey; + tokenProgram: PublicKey; +} + +export function getTransferWrappedAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array +): TransferWrappedAccounts { + const mint = deriveWrappedMintKey( + tokenBridgeProgramId, + tokenChain, + tokenAddress + ); + const { + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageCpiAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + from: new PublicKey(from), + fromOwner: new PublicKey(fromOwner), + mint: mint, + wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId), + wormholeBridge, + wormholeMessage: wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + wormholeProgram: new PublicKey(wormholeProgramId), + tokenProgram: TOKEN_PROGRAM_ID, + }; +} diff --git a/sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts b/sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts new file mode 100644 index 000000000..209870458 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/instructions/transferWrappedWithPayload.ts @@ -0,0 +1,136 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { createReadOnlyTokenBridgeProgramInterface } from "../program"; +import { getPostMessageCpiAccounts } from "../../wormhole"; +import { + deriveAuthoritySignerKey, + deriveSenderAccountKey, + deriveTokenBridgeConfigKey, + deriveWrappedMetaKey, + deriveWrappedMintKey, +} from "../accounts"; + +export function createTransferWrappedWithPayloadInstruction( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + nonce: number, + amount: bigint, + targetAddress: Buffer | Uint8Array, + targetChain: number, + payload: Buffer | Uint8Array +): TransactionInstruction { + const methods = createReadOnlyTokenBridgeProgramInterface( + tokenBridgeProgramId + ).methods.transferWrappedWithPayload( + nonce, + amount as any, + Buffer.from(targetAddress) as any, + targetChain, + Buffer.from(payload) as any, + null + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferWrappedWithPayloadAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message, + from, + fromOwner, + tokenChain, + tokenAddress + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferWrappedWithPayloadAccounts { + payer: PublicKey; + config: PublicKey; + from: PublicKey; + fromOwner: PublicKey; + mint: PublicKey; + wrappedMeta: PublicKey; + authoritySigner: PublicKey; + wormholeBridge: PublicKey; + wormholeMessage: PublicKey; + wormholeEmitter: PublicKey; + wormholeSequence: PublicKey; + wormholeFeeCollector: PublicKey; + clock: PublicKey; + sender: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; + tokenProgram: PublicKey; + wormholeProgram: PublicKey; +} + +export function getTransferWrappedWithPayloadAccounts( + tokenBridgeProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData, + from: PublicKeyInitData, + fromOwner: PublicKeyInitData, + tokenChain: number, + tokenAddress: Buffer | Uint8Array, + cpiProgramId?: PublicKeyInitData +): TransferWrappedWithPayloadAccounts { + const mint = deriveWrappedMintKey( + tokenBridgeProgramId, + tokenChain, + tokenAddress + ); + const { + wormholeBridge, + wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + rent, + systemProgram, + } = getPostMessageCpiAccounts( + tokenBridgeProgramId, + wormholeProgramId, + payer, + message + ); + return { + payer: new PublicKey(payer), + config: deriveTokenBridgeConfigKey(tokenBridgeProgramId), + from: new PublicKey(from), + fromOwner: new PublicKey(fromOwner), + mint: mint, + wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint), + authoritySigner: deriveAuthoritySignerKey(tokenBridgeProgramId), + wormholeBridge, + wormholeMessage: wormholeMessage, + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector, + clock, + sender: new PublicKey( + cpiProgramId === undefined ? payer : deriveSenderAccountKey(cpiProgramId) + ), + rent, + systemProgram, + wormholeProgram: new PublicKey(wormholeProgramId), + tokenProgram: TOKEN_PROGRAM_ID, + }; +} diff --git a/sdk/js/src/solana/tokenBridge/program.ts b/sdk/js/src/solana/tokenBridge/program.ts new file mode 100644 index 000000000..7611f1182 --- /dev/null +++ b/sdk/js/src/solana/tokenBridge/program.ts @@ -0,0 +1,33 @@ +import { Connection, PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { Program, Provider } from "@project-serum/anchor"; +import { createReadOnlyProvider } from "../utils"; +import { TokenBridgeCoder } from "./coder"; +import { TokenBridge } from "../types/tokenBridge"; + +import IDL from "../../anchor-idl/token_bridge.json"; + +export function createTokenBridgeProgramInterface( + programId: PublicKeyInitData, + provider?: Provider +): Program { + return new Program( + IDL as TokenBridge, + new PublicKey(programId), + provider === undefined ? ({ connection: null } as any) : provider, + coder() + ); +} + +export function createReadOnlyTokenBridgeProgramInterface( + programId: PublicKeyInitData, + connection?: Connection +): Program { + return createTokenBridgeProgramInterface( + programId, + createReadOnlyProvider(connection) + ); +} + +export function coder(): TokenBridgeCoder { + return new TokenBridgeCoder(IDL as TokenBridge); +} diff --git a/sdk/js/src/solana/utils/account.ts b/sdk/js/src/solana/utils/account.ts new file mode 100644 index 000000000..957099c58 --- /dev/null +++ b/sdk/js/src/solana/utils/account.ts @@ -0,0 +1,69 @@ +import { + PublicKey, + AccountMeta, + AccountInfo, + PublicKeyInitData, +} from "@solana/web3.js"; + +/** + * Find valid program address. See {@link PublicKey.findProgramAddressSync} for details. + * + * @param {(Buffer | Uint8Array)[]} seeds - seeds for PDA + * @param {PublicKeyInitData} programId - program address + * @returns PDA + */ +export function deriveAddress( + seeds: (Buffer | Uint8Array)[], + programId: PublicKeyInitData +): PublicKey { + return PublicKey.findProgramAddressSync(seeds, new PublicKey(programId))[0]; +} + +/** + * Factory to create AccountMeta with `isWritable` set to `true` + * + * @param {PublicKEyInitData} pubkey - account address + * @param {boolean} isSigner - whether account authorized transaction + * @returns metadata for writable account + */ +export function newAccountMeta( + pubkey: PublicKeyInitData, + isSigner: boolean +): AccountMeta { + return { + pubkey: new PublicKey(pubkey), + isWritable: true, + isSigner, + }; +} + +/** + * Factory to create AccountMeta with `isWritable` set to `false` + * + * @param {PublicKEyInitData} pubkey - account address + * @param {boolean} isSigner - whether account authorized transaction + * @returns metadata for read-only account + */ +export function newReadOnlyAccountMeta( + pubkey: PublicKeyInitData, + isSigner: boolean +): AccountMeta { + return { + pubkey: new PublicKey(pubkey), + isWritable: false, + isSigner, + }; +} + +/** + * Get serialized data from account + * + * @param {AccountInfo} info - Solana AccountInfo + * @returns serialized data as Buffer + */ +export function getAccountData(info: AccountInfo | null): Buffer { + if (info === null) { + throw Error("account info is null"); + } + return info.data; +} diff --git a/sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts b/sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts new file mode 100644 index 000000000..a290ecd7a --- /dev/null +++ b/sdk/js/src/solana/utils/bpfLoaderUpgradeable.ts @@ -0,0 +1,23 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "./account"; + +export class BpfLoaderUpgradeable { + /** + * @internal + */ + constructor() {} + + /** + * Public key that identifies the SPL Token Metadata program + */ + static programId: PublicKey = new PublicKey( + "BPFLoaderUpgradeab1e11111111111111111111111" + ); +} + +export function deriveUpgradeableProgramKey(programId: PublicKeyInitData) { + return deriveAddress( + [new PublicKey(programId).toBuffer()], + BpfLoaderUpgradeable.programId + ); +} diff --git a/sdk/js/src/solana/utils/connection.ts b/sdk/js/src/solana/utils/connection.ts new file mode 100644 index 000000000..2471a1b4b --- /dev/null +++ b/sdk/js/src/solana/utils/connection.ts @@ -0,0 +1,12 @@ +import { Provider } from "@project-serum/anchor"; +import { Connection } from "@solana/web3.js"; + +export function createReadOnlyProvider( + connection?: Connection +): Provider | undefined { + if (connection === undefined) { + return undefined; + } + + return { connection }; +} diff --git a/sdk/js/src/solana/utils/index.ts b/sdk/js/src/solana/utils/index.ts new file mode 100644 index 000000000..a772221dc --- /dev/null +++ b/sdk/js/src/solana/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./account"; +export * from "./bpfLoaderUpgradeable"; +export * from "./connection"; +export * from "./secp256k1"; +export * from "./splMetadata"; +export * from "./transaction"; diff --git a/sdk/js/src/solana/utils/secp256k1.ts b/sdk/js/src/solana/utils/secp256k1.ts new file mode 100644 index 000000000..3b802f590 --- /dev/null +++ b/sdk/js/src/solana/utils/secp256k1.ts @@ -0,0 +1,124 @@ +import { TransactionInstruction, Secp256k1Program } from "@solana/web3.js"; + +export const SIGNATURE_LENGTH = 65; +export const ETHEREUM_KEY_LENGTH = 20; + +/** + * Create {@link TransactionInstruction} for {@link Secp256k1Program}. + * + * @param {Buffer[]} signatures - 65-byte signatures (64 bytes + 1 byte recovery id) + * @param {Buffer[]} keys - 20-byte ethereum public keys + * @param {Buffer} message - 32-byte hash + * @returns Solana instruction for Secp256k1 program + */ +export function createSecp256k1Instruction( + signatures: Buffer[], + keys: Buffer[], + message: Buffer +): TransactionInstruction { + return { + keys: [], + programId: Secp256k1Program.programId, + data: Secp256k1SignatureOffsets.serialize(signatures, keys, message), + }; +} + +/** + * Secp256k1SignatureOffsets serializer + * + * See {@link https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program} for more info. + */ +export class Secp256k1SignatureOffsets { + // https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program + // + // struct Secp256k1SignatureOffsets { + // secp_signature_key_offset: u16, // offset to [signature,recovery_id,etherum_address] of 64+1+20 bytes + // secp_signature_instruction_index: u8, // instruction index to find data + // secp_pubkey_offset: u16, // offset to [signature,recovery_id] of 64+1 bytes + // secp_signature_instruction_index: u8, // instruction index to find data + // secp_message_data_offset: u16, // offset to start of message data + // secp_message_data_size: u16, // size of message data + // secp_message_instruction_index: u8, // index of instruction data to get message data + // } + // + // Pseudo code of the operation: + // + // process_instruction() { + // for i in 0..count { + // // i'th index values referenced: + // instructions = &transaction.message().instructions + // signature = instructions[secp_signature_instruction_index].data[secp_signature_offset..secp_signature_offset + 64] + // recovery_id = instructions[secp_signature_instruction_index].data[secp_signature_offset + 64] + // ref_eth_pubkey = instructions[secp_pubkey_instruction_index].data[secp_pubkey_offset..secp_pubkey_offset + 32] + // message_hash = keccak256(instructions[secp_message_instruction_index].data[secp_message_data_offset..secp_message_data_offset + secp_message_data_size]) + // pubkey = ecrecover(signature, recovery_id, message_hash) + // eth_pubkey = keccak256(pubkey[1..])[12..] + // if eth_pubkey != ref_eth_pubkey { + // return Error + // } + // } + // return Success + // } + + /** + * Serialize multiple signatures, ethereum public keys and message as Secp256k1 instruction data. + * + * @param {Buffer[]} signatures - 65-byte signatures (64 + 1 recovery id) + * @param {Buffer[]} keys - ethereum public keys + * @param {Buffer} message - 32-byte hash + * @returns serialized Secp256k1 instruction data + */ + static serialize(signatures: Buffer[], keys: Buffer[], message: Buffer) { + if (signatures.length == 0) { + throw Error("signatures.length == 0"); + } + + if (signatures.length != keys.length) { + throw Error("signatures.length != keys.length"); + } + + if (message.length != 32) { + throw Error("message.length != 32"); + } + + const numSignatures = signatures.length; + const offsetSpan = 11; + const dataLoc = 1 + numSignatures * offsetSpan; + + const dataLen = SIGNATURE_LENGTH + ETHEREUM_KEY_LENGTH; // 65 signature size + 20 eth pubkey size + const messageDataOffset = dataLoc + numSignatures * dataLen; + const messageDataSize = 32; + const serialized = Buffer.alloc(messageDataOffset + messageDataSize); + + serialized.writeUInt8(numSignatures, 0); + serialized.write(message.toString("hex"), messageDataOffset, "hex"); + + for (let i = 0; i < numSignatures; ++i) { + const signature = signatures.at(i); + if (signature?.length != SIGNATURE_LENGTH) { + throw Error(`signatures[${i}].length != 65`); + } + + const key = keys.at(i); + if (key?.length != ETHEREUM_KEY_LENGTH) { + throw Error(`keys[${i}].length != 20`); + } + + const signatureOffset = dataLoc + dataLen * i; + const ethAddressOffset = signatureOffset + 65; + + serialized.writeUInt16LE(signatureOffset, 1 + i * offsetSpan); + serialized.writeUInt8(0, 3 + i * offsetSpan); + serialized.writeUInt16LE(ethAddressOffset, 4 + i * offsetSpan); + serialized.writeUInt8(0, 6 + i * offsetSpan); + serialized.writeUInt16LE(messageDataOffset, 7 + i * offsetSpan); + serialized.writeUInt16LE(messageDataSize, 9 + i * offsetSpan); + serialized.writeUInt8(0, 10 + i * offsetSpan); + + serialized.write(signature.toString("hex"), signatureOffset, "hex"); + serialized.write(key.toString("hex"), ethAddressOffset, "hex"); + } + + return serialized; + } +} diff --git a/sdk/js/src/solana/utils/splMetadata.ts b/sdk/js/src/solana/utils/splMetadata.ts new file mode 100644 index 000000000..b7e3daef8 --- /dev/null +++ b/sdk/js/src/solana/utils/splMetadata.ts @@ -0,0 +1,331 @@ +import { + AccountMeta, + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { + deriveAddress, + getAccountData, + newAccountMeta, + newReadOnlyAccountMeta, +} from "./account"; + +export class Creator { + address: PublicKey; + verified: boolean; + share: number; + + constructor(address: PublicKeyInitData, verified: boolean, share: number) { + this.address = new PublicKey(address); + this.verified = verified; + this.share = share; + } + + static size: number = 34; + + serialize() { + const serialized = Buffer.alloc(Creator.size); + serialized.write(this.address.toBuffer().toString("hex"), 0, "hex"); + if (this.verified) { + serialized.writeUInt8(1, 32); + } + serialized.writeUInt8(this.share, 33); + return serialized; + } + + static deserialize(data: Buffer): Creator { + const address = data.subarray(0, 32); + const verified = data.readUInt8(32) > 0; + const share = data.readUInt8(33); + return new Creator(address, verified, share); + } +} + +export class Data { + name: string; + symbol: string; + uri: string; + sellerFeeBasisPoints: number; + creators: Creator[] | null; + + constructor( + name: string, + symbol: string, + uri: string, + sellerFeeBasisPoints: number, + creators: Creator[] | null + ) { + this.name = name; + this.symbol = symbol; + this.uri = uri; + this.sellerFeeBasisPoints = sellerFeeBasisPoints; + this.creators = creators; + } + + serialize() { + const nameLen = this.name.length; + const symbolLen = this.symbol.length; + const uriLen = this.uri.length; + const creators = this.creators; + const [creatorsLen, creatorsSize] = (() => { + if (creators === null) { + return [0, 0]; + } + + const creatorsLen = creators.length; + return [creatorsLen, 4 + creatorsLen * Creator.size]; + })(); + const serialized = Buffer.alloc( + 15 + nameLen + symbolLen + uriLen + creatorsSize + ); + serialized.writeUInt32LE(nameLen, 0); + serialized.write(this.name, 4); + serialized.writeUInt32LE(symbolLen, 4 + nameLen); + serialized.write(this.symbol, 8 + nameLen); + serialized.writeUInt32LE(uriLen, 8 + nameLen + symbolLen); + serialized.write(this.uri, 12 + nameLen + symbolLen); + serialized.writeUInt16LE( + this.sellerFeeBasisPoints, + 12 + nameLen + symbolLen + uriLen + ); + if (creators === null) { + serialized.writeUInt8(0, 14 + nameLen + symbolLen + uriLen); + } else { + serialized.writeUInt8(1, 14 + nameLen + symbolLen + uriLen); + serialized.writeUInt32LE(creatorsLen, 15 + nameLen + symbolLen + uriLen); + for (let i = 0; i < creatorsLen; ++i) { + const creator = creators.at(i)!; + const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size; + serialized.write(creator.serialize().toString("hex"), idx, "hex"); + } + } + return serialized; + } + + static deserialize(data: Buffer): Data { + const nameLen = data.readUInt32LE(0); + const name = data.subarray(4, 4 + nameLen).toString(); + const symbolLen = data.readUInt32LE(4 + nameLen); + const symbol = data + .subarray(8 + nameLen, 8 + nameLen + symbolLen) + .toString(); + const uriLen = data.readUInt32LE(8 + nameLen + symbolLen); + const uri = data + .subarray(12 + nameLen + symbolLen, 12 + nameLen + symbolLen + uriLen) + .toString(); + const sellerFeeBasisPoints = data.readUInt16LE( + 12 + nameLen + symbolLen + uriLen + ); + const optionCreators = data.readUInt8(14 + nameLen + symbolLen + uriLen); + const creators = (() => { + if (optionCreators == 0) { + return null; + } + + const creators: Creator[] = []; + const creatorsLen = data.readUInt32LE(15 + nameLen + symbolLen + uriLen); + for (let i = 0; i < creatorsLen; ++i) { + const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size; + creators.push( + Creator.deserialize(data.subarray(idx, idx + Creator.size)) + ); + } + return creators; + })(); + return new Data(name, symbol, uri, sellerFeeBasisPoints, creators); + } +} + +export class CreateMetadataAccountArgs extends Data { + isMutable: boolean; + + constructor( + name: string, + symbol: string, + uri: string, + sellerFeeBasisPoints: number, + creators: Creator[] | null, + isMutable: boolean + ) { + super(name, symbol, uri, sellerFeeBasisPoints, creators); + this.isMutable = isMutable; + } + + static serialize( + name: string, + symbol: string, + uri: string, + sellerFeeBasisPoints: number, + creators: Creator[] | null, + isMutable: boolean + ) { + return new CreateMetadataAccountArgs( + name, + symbol, + uri, + sellerFeeBasisPoints, + creators, + isMutable + ).serialize(); + } + + static serializeInstructionData( + name: string, + symbol: string, + uri: string, + sellerFeeBasisPoints: number, + creators: Creator[] | null, + isMutable: boolean + ) { + return Buffer.concat([ + Buffer.alloc(1, 0), + CreateMetadataAccountArgs.serialize( + name, + symbol, + uri, + sellerFeeBasisPoints, + creators, + isMutable + ), + ]); + } + + serialize() { + return Buffer.concat([ + super.serialize(), + Buffer.alloc(1, this.isMutable ? 1 : 0), + ]); + } +} + +export class SplTokenMetadataProgram { + /** + * @internal + */ + constructor() {} + + /** + * Public key that identifies the SPL Token Metadata program + */ + static programId: PublicKey = new PublicKey( + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + ); + + static createMetadataAccounts( + payer: PublicKey, + mint: PublicKey, + mintAuthority: PublicKey, + name: string, + symbol: string, + updateAuthority: PublicKey, + updateAuthorityIsSigner: boolean = false, + uri?: string, + creators?: Creator[] | null, + sellerFeeBasisPoints?: number, + isMutable: boolean = false, + metadataAccount: PublicKey = deriveSplTokenMetadataKey(mint) + ): TransactionInstruction { + const keys: AccountMeta[] = [ + newAccountMeta(metadataAccount, false), + newReadOnlyAccountMeta(mint, false), + newReadOnlyAccountMeta(mintAuthority, true), + newReadOnlyAccountMeta(payer, true), + newReadOnlyAccountMeta(updateAuthority, updateAuthorityIsSigner), + newReadOnlyAccountMeta(SystemProgram.programId, false), + newReadOnlyAccountMeta(SYSVAR_RENT_PUBKEY, false), + ]; + const data = CreateMetadataAccountArgs.serializeInstructionData( + name, + symbol, + uri === undefined ? "" : uri, + sellerFeeBasisPoints === undefined ? 0 : sellerFeeBasisPoints, + creators === undefined ? null : creators, + isMutable + ); + return { + programId: SplTokenMetadataProgram.programId, + keys, + data, + }; + } +} + +export function deriveSplTokenMetadataKey(mint: PublicKeyInitData): PublicKey { + return deriveAddress( + [ + Buffer.from("metadata"), + SplTokenMetadataProgram.programId.toBuffer(), + new PublicKey(mint).toBuffer(), + ], + SplTokenMetadataProgram.programId + ); +} + +export enum Key { + Uninitialized, + EditionV1, + MasterEditionV1, + ReservationListV1, + MetadataV1, + ReservationListV2, + MasterEditionV2, + EditionMarker, +} + +export class Metadata { + key: Key; + updateAuthority: PublicKey; + mint: PublicKey; + data: Data; + primarySaleHappened: boolean; + isMutable: boolean; + + constructor( + key: number, + updateAuthority: PublicKeyInitData, + mint: PublicKeyInitData, + data: Data, + primarySaleHappened: boolean, + isMutable: boolean + ) { + this.key = key as Key; + this.updateAuthority = new PublicKey(updateAuthority); + this.mint = new PublicKey(mint); + this.data = data; + this.primarySaleHappened = primarySaleHappened; + this.isMutable = isMutable; + } + + static deserialize(data: Buffer): Metadata { + const key = data.readUInt8(0); + const updateAuthority = data.subarray(1, 33); + const mint = data.subarray(33, 65); + const meta = Data.deserialize(data.subarray(65)); + const metaLen = meta.serialize().length; + const primarySaleHappened = data.readUInt8(65 + metaLen) > 0; + const isMutable = data.readUInt8(66 + metaLen) > 0; + return new Metadata( + key, + updateAuthority, + mint, + meta, + primarySaleHappened, + isMutable + ); + } +} + +export async function getMetadata( + connection: Connection, + mint: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(deriveSplTokenMetadataKey(mint), commitment) + .then((info) => Metadata.deserialize(getAccountData(info))); +} diff --git a/sdk/js/src/solana/utils/transaction.ts b/sdk/js/src/solana/utils/transaction.ts new file mode 100644 index 000000000..d7967a875 --- /dev/null +++ b/sdk/js/src/solana/utils/transaction.ts @@ -0,0 +1,208 @@ +import { + Transaction, + Keypair, + Connection, + PublicKeyInitData, + PublicKey, + ConfirmOptions, + RpcResponseAndContext, + SignatureResult, + TransactionSignature, + Signer, +} from "@solana/web3.js"; + +/** + * Object that holds list of unsigned {@link Transaction}s and {@link Keypair}s + * required to sign for each transaction. + */ +export interface PreparedTransactions { + unsignedTransactions: Transaction[]; + signers: Signer[]; +} + +export interface TransactionSignatureAndResponse { + signature: TransactionSignature; + response: RpcResponseAndContext; +} + +/** + * Resembles WalletContextState and Anchor's NodeWallet's signTransaction function signature + */ +export type SignTransaction = ( + transaction: Transaction +) => Promise; + +/** + * + * @param payers + * @returns + */ +export function signTransactionFactory(...payers: Signer[]): SignTransaction { + return modifySignTransaction( + (transaction: Transaction) => Promise.resolve(transaction), + ...payers + ); +} + +export function modifySignTransaction( + signTransaction: SignTransaction, + ...payers: Signer[] +): SignTransaction { + return (transaction: Transaction) => { + for (const payer of payers) { + transaction.partialSign(payer); + } + return signTransaction(transaction); + }; +} + +/** + * Wrapper for {@link Keypair} resembling Solana web3 browser wallet + */ +export class NodeWallet { + payer: Keypair; + signTransaction: SignTransaction; + + constructor(payer: Keypair) { + this.payer = payer; + this.signTransaction = signTransactionFactory(this.payer); + } + + static fromSecretKey( + secretKey: Uint8Array, + options?: + | { + skipValidation?: boolean | undefined; + } + | undefined + ): NodeWallet { + return new NodeWallet(Keypair.fromSecretKey(secretKey, options)); + } + + publicKey(): PublicKey { + return this.payer.publicKey; + } + + pubkey(): PublicKey { + return this.publicKey(); + } + + key(): PublicKey { + return this.publicKey(); + } + + toString(): string { + return this.publicKey().toString(); + } + + keypair(): Keypair { + return this.payer; + } + + signer(): Signer { + return this.keypair(); + } +} + +/** + * The transactions provided to this function should be ready to send. + * This function will do the following: + * 1. Add the {@param payer} as the feePayer and latest blockhash to the {@link Transaction}. + * 2. Sign using {@param signTransaction}. + * 3. Send raw transaction. + * 4. Confirm transaction. + */ +export async function signSendAndConfirmTransaction( + connection: Connection, + payer: PublicKeyInitData, + signTransaction: SignTransaction, + unsignedTransaction: Transaction, + options?: ConfirmOptions +): Promise { + const commitment = options?.commitment; + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(commitment); + unsignedTransaction.recentBlockhash = blockhash; + unsignedTransaction.feePayer = new PublicKey(payer); + + // Sign transaction, broadcast, and confirm + const signed = await signTransaction(unsignedTransaction); + const signature = await connection.sendRawTransaction( + signed.serialize(), + options + ); + const response = await connection.confirmTransaction( + { + blockhash, + lastValidBlockHeight, + signature, + }, + commitment + ); + return { signature, response }; +} + +/** + * @deprecated Please use {@link signSendAndConfirmTransaction} instead, which allows + * retries to be configured in {@link ConfirmOptions}. + * + * The transactions provided to this function should be ready to send. + * This function will do the following: + * 1. Add the {@param payer} as the feePayer and latest blockhash to the {@link Transaction}. + * 2. Sign using {@param signTransaction}. + * 3. Send raw transaction. + * 4. Confirm transaction. + */ +export async function sendAndConfirmTransactionsWithRetry( + connection: Connection, + signTransaction: SignTransaction, + payer: string, + unsignedTransactions: Transaction[], + maxRetries: number = 0, + options?: ConfirmOptions +): Promise { + if (unsignedTransactions.length == 0) { + return Promise.reject("No transactions provided to send."); + } + + const commitment = options?.commitment; + + let currentRetries = 0; + const output: TransactionSignatureAndResponse[] = []; + for (const transaction of unsignedTransactions) { + while (currentRetries <= maxRetries) { + try { + const latest = await connection.getLatestBlockhash(commitment); + transaction.recentBlockhash = latest.blockhash; + transaction.feePayer = new PublicKey(payer); + + const signed = await signTransaction(transaction).catch((e) => null); + if (signed === null) { + return Promise.reject("Failed to sign transaction."); + } + + const signature = await connection.sendRawTransaction( + signed.serialize(), + options + ); + const response = await connection.confirmTransaction( + { + signature, + ...latest, + }, + commitment + ); + output.push({ signature, response }); + break; + } catch (e) { + console.error(e); + ++currentRetries; + } + } + if (currentRetries > maxRetries) { + return Promise.reject("Reached the maximum number of retries."); + } + } + + return Promise.resolve(output); +} diff --git a/sdk/js/src/solana/wasm.ts b/sdk/js/src/solana/wasm.ts deleted file mode 100644 index 327b3eaca..000000000 --- a/sdk/js/src/solana/wasm.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@certusone/wormhole-sdk-wasm"; diff --git a/sdk/js/src/solana/wormhole/accounts/claim.ts b/sdk/js/src/solana/wormhole/accounts/claim.ts new file mode 100644 index 000000000..6c41a7596 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/claim.ts @@ -0,0 +1,54 @@ +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, +} from "@solana/web3.js"; +import { deriveAddress, getAccountData } from "../../utils"; + +export function deriveClaimKey( + programId: PublicKeyInitData, + emitterAddress: Buffer | Uint8Array | string, + emitterChain: number, + sequence: bigint | number +): PublicKey { + const address = + typeof emitterAddress == "string" + ? Buffer.from(emitterAddress, "hex") + : Buffer.from(emitterAddress); + if (address.length != 32) { + throw Error("address.length != 32"); + } + const sequenceSerialized = Buffer.alloc(8); + sequenceSerialized.writeBigInt64BE( + typeof sequence == "number" ? BigInt(sequence) : sequence + ); + return deriveAddress( + [ + address, + (() => { + const buf = Buffer.alloc(2); + buf.writeUInt16BE(emitterChain as number); + return buf; + })(), + sequenceSerialized, + ], + programId + ); +} + +export async function getClaim( + connection: Connection, + programId: PublicKeyInitData, + emitterAddress: Buffer | Uint8Array | string, + emitterChain: number, + sequence: bigint | number, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo( + deriveClaimKey(programId, emitterAddress, emitterChain, sequence), + commitment + ) + .then((info) => !!getAccountData(info)[0]); +} diff --git a/sdk/js/src/solana/wormhole/accounts/config.ts b/sdk/js/src/solana/wormhole/accounts/config.ts new file mode 100644 index 000000000..4f05b0f72 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/config.ts @@ -0,0 +1,68 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { deriveAddress, getAccountData } from "../../utils"; + +export function deriveWormholeBridgeDataKey( + wormholeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("Bridge")], wormholeProgramId); +} + +export async function getWormholeBridgeData( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(deriveWormholeBridgeDataKey(wormholeProgramId), commitment) + .then((info) => BridgeData.deserialize(getAccountData(info))); +} + +export class BridgeConfig { + guardianSetExpirationTime: number; + fee: bigint; + + constructor(guardianSetExpirationTime: number, fee: bigint) { + this.guardianSetExpirationTime = guardianSetExpirationTime; + this.fee = fee; + } + + static deserialize(data: Buffer): BridgeConfig { + if (data.length != 12) { + throw new Error("data.length != 12"); + } + const guardianSetExpirationTime = data.readUInt32LE(0); + const fee = data.readBigUInt64LE(4); + return new BridgeConfig(guardianSetExpirationTime, fee); + } +} + +export class BridgeData { + guardianSetIndex: number; + lastLamports: bigint; + config: BridgeConfig; + + constructor( + guardianSetIndex: number, + lastLamports: bigint, + config: BridgeConfig + ) { + this.guardianSetIndex = guardianSetIndex; + this.lastLamports = lastLamports; + this.config = config; + } + + static deserialize(data: Buffer): BridgeData { + if (data.length != 24) { + throw new Error("data.length != 24"); + } + const guardianSetIndex = data.readUInt32LE(0); + const lastLamports = data.readBigUInt64LE(4); + const config = BridgeConfig.deserialize(data.subarray(12)); + return new BridgeData(guardianSetIndex, lastLamports, config); + } +} diff --git a/sdk/js/src/solana/wormhole/accounts/emitter.ts b/sdk/js/src/solana/wormhole/accounts/emitter.ts new file mode 100644 index 000000000..0d44a5d0b --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/emitter.ts @@ -0,0 +1,34 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export interface EmitterAccounts { + emitter: PublicKey; + sequence: PublicKey; +} + +export function deriveWormholeEmitterKey( + emitterProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("emitter")], emitterProgramId); +} + +export function deriveEmitterSequenceKey( + emitter: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress( + [Buffer.from("Sequence"), new PublicKey(emitter).toBytes()], + wormholeProgramId + ); +} + +export function getEmitterKeys( + emitterProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): EmitterAccounts { + const emitter = deriveWormholeEmitterKey(emitterProgramId); + return { + emitter, + sequence: deriveEmitterSequenceKey(emitter, wormholeProgramId), + }; +} diff --git a/sdk/js/src/solana/wormhole/accounts/feeCollector.ts b/sdk/js/src/solana/wormhole/accounts/feeCollector.ts new file mode 100644 index 000000000..a53b6e5b6 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/feeCollector.ts @@ -0,0 +1,8 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export function deriveFeeCollectorKey( + wormholeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("fee_collector")], wormholeProgramId); +} diff --git a/sdk/js/src/solana/wormhole/accounts/guardianSet.ts b/sdk/js/src/solana/wormhole/accounts/guardianSet.ts new file mode 100644 index 000000000..1c953ed09 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/guardianSet.ts @@ -0,0 +1,73 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { + ETHEREUM_KEY_LENGTH, + deriveAddress, + getAccountData, +} from "../../utils"; + +export function deriveGuardianSetKey( + wormholeProgramId: PublicKeyInitData, + index: number +): PublicKey { + return deriveAddress( + [ + Buffer.from("GuardianSet"), + (() => { + const buf = Buffer.alloc(4); + buf.writeUInt32BE(index); + return buf; + })(), + ], + wormholeProgramId + ); +} + +export async function getGuardianSet( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + index: number, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(deriveGuardianSetKey(wormholeProgramId, index), commitment) + .then((info) => GuardianSetData.deserialize(getAccountData(info))); +} + +export class GuardianSetData { + index: number; + keys: Buffer[]; + creationTime: number; + expirationTime: number; + + constructor( + index: number, + keys: Buffer[], + creationTime: number, + expirationTime: number + ) { + this.index = index; + this.keys = keys; + this.creationTime = creationTime; + this.expirationTime = expirationTime; + } + + static deserialize(data: Buffer): GuardianSetData { + const index = data.readUInt32LE(0); + const keysLen = data.readUInt32LE(4); + const keysEnd = 8 + keysLen * ETHEREUM_KEY_LENGTH; + const creationTime = data.readUInt32LE(keysEnd); + const expirationTime = data.readUInt32LE(4 + keysEnd); + + const keys = []; + for (let i = 0; i < keysLen; ++i) { + const start = 8 + i * ETHEREUM_KEY_LENGTH; + keys.push(data.subarray(start, start + ETHEREUM_KEY_LENGTH)); + } + return new GuardianSetData(index, keys, creationTime, expirationTime); + } +} diff --git a/sdk/js/src/solana/wormhole/accounts/index.ts b/sdk/js/src/solana/wormhole/accounts/index.ts new file mode 100644 index 000000000..3330f5f57 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/index.ts @@ -0,0 +1,8 @@ +export * from "./claim"; +export * from "./config"; +export * from "./emitter"; +export * from "./feeCollector"; +export * from "./guardianSet"; +export * from "./postedVaa"; +export * from "./signatureSet"; +export * from "./upgrade"; diff --git a/sdk/js/src/solana/wormhole/accounts/postedVaa.ts b/sdk/js/src/solana/wormhole/accounts/postedVaa.ts new file mode 100644 index 000000000..58c55956d --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/postedVaa.ts @@ -0,0 +1,50 @@ +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, +} from "@solana/web3.js"; +import { deriveAddress, getAccountData } from "../../utils"; +import { MessageData } from "../message"; + +export class PostedMessageData { + message: MessageData; + + constructor(message: MessageData) { + this.message = message; + } + + static deserialize(data: Buffer) { + return new PostedMessageData(MessageData.deserialize(data.subarray(3))); + } +} + +export class PostedVaaData extends PostedMessageData {} + +export function derivePostedVaaKey( + wormholeProgramId: PublicKeyInitData, + hash: Buffer +): PublicKey { + return deriveAddress([Buffer.from("PostedVAA"), hash], wormholeProgramId); +} + +export async function getPostedVaa( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + hash: Buffer, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(derivePostedVaaKey(wormholeProgramId, hash), commitment) + .then((info) => PostedVaaData.deserialize(getAccountData(info))); +} + +export async function getPostedMessage( + connection: Connection, + messageKey: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(new PublicKey(messageKey), commitment) + .then((info) => PostedMessageData.deserialize(getAccountData(info))); +} diff --git a/sdk/js/src/solana/wormhole/accounts/signatureSet.ts b/sdk/js/src/solana/wormhole/accounts/signatureSet.ts new file mode 100644 index 000000000..7cf9a3348 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/signatureSet.ts @@ -0,0 +1,40 @@ +import { + Connection, + PublicKey, + Commitment, + PublicKeyInitData, +} from "@solana/web3.js"; +import { getAccountData } from "../../utils"; + +export async function getSignatureSetData( + connection: Connection, + signatureSet: PublicKeyInitData, + commitment?: Commitment +): Promise { + return connection + .getAccountInfo(new PublicKey(signatureSet), commitment) + .then((info) => SignatureSetData.deserialize(getAccountData(info))); +} + +export class SignatureSetData { + signatures: boolean[]; + hash: Buffer; + guardianSetIndex: number; + + constructor(signatures: boolean[], hash: Buffer, guardianSetIndex: number) { + this.signatures = signatures; + this.hash = hash; + this.guardianSetIndex = guardianSetIndex; + } + + static deserialize(data: Buffer): SignatureSetData { + const numSignatures = data.readUInt32LE(0); + const signatures = [...data.subarray(4, 4 + numSignatures)].map( + (x) => x != 0 + ); + const hashIndex = 4 + numSignatures; + const hash = data.subarray(hashIndex, hashIndex + 32); + const guardianSetIndex = data.readUInt32LE(hashIndex + 32); + return new SignatureSetData(signatures, hash, guardianSetIndex); + } +} diff --git a/sdk/js/src/solana/wormhole/accounts/upgrade.ts b/sdk/js/src/solana/wormhole/accounts/upgrade.ts new file mode 100644 index 000000000..7fc396005 --- /dev/null +++ b/sdk/js/src/solana/wormhole/accounts/upgrade.ts @@ -0,0 +1,8 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { deriveAddress } from "../../utils"; + +export function deriveUpgradeAuthorityKey( + wormholeProgramId: PublicKeyInitData +): PublicKey { + return deriveAddress([Buffer.from("upgrade")], wormholeProgramId); +} diff --git a/sdk/js/src/solana/wormhole/coder/accounts.ts b/sdk/js/src/solana/wormhole/coder/accounts.ts new file mode 100644 index 000000000..75205d456 --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/accounts.ts @@ -0,0 +1,78 @@ +import { AccountsCoder, Idl } from "@project-serum/anchor"; +import { accountSize, IdlTypeDef } from "../../anchor"; + +export class WormholeAccountsCoder + implements AccountsCoder +{ + constructor(private idl: Idl) {} + + public async encode(accountName: A, account: T): Promise { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public decode(accountName: A, ix: Buffer): T { + return this.decodeUnchecked(accountName, ix); + } + + public decodeUnchecked(accountName: A, ix: Buffer): T { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public memcmp(accountName: A, _appendData?: Buffer): any { + switch (accountName) { + case "postVaa": { + return { + dataSize: 56, // + 4 + payload.length + }; + } + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public size(idlAccount: IdlTypeDef): number { + return accountSize(this.idl, idlAccount) ?? 0; + } +} + +export interface PostVAAData { + version: number; + guardianSetIndex: number; + timestamp: number; + nonce: number; + emitterChain: number; + emitterAddress: Buffer; + sequence: bigint; + consistencyLevel: number; + payload: Buffer; +} + +function encodePostVaaData(account: PostVAAData): Buffer { + const payload = account.payload; + const serialized = Buffer.alloc(60 + payload.length); + serialized.writeUInt8(account.version, 0); + serialized.writeUInt32LE(account.guardianSetIndex, 1); + serialized.writeUInt32LE(account.timestamp, 5); + serialized.writeUInt32LE(account.nonce, 9); + serialized.writeUInt16LE(account.emitterChain, 13); + serialized.write(account.emitterAddress.toString("hex"), 15, "hex"); + serialized.writeBigInt64LE(account.sequence, 47); + serialized.writeUInt8(account.consistencyLevel, 55); + serialized.writeUInt32LE(payload.length, 56); + serialized.write(payload.toString("hex"), 60, "hex"); + + return serialized; +} + +function decodePostVaaAccount(buf: Buffer): T { + return {} as T; +} diff --git a/sdk/js/src/solana/wormhole/coder/events.ts b/sdk/js/src/solana/wormhole/coder/events.ts new file mode 100644 index 000000000..a76314112 --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/events.ts @@ -0,0 +1,10 @@ +import { EventCoder, Event, Idl } from "@project-serum/anchor"; +import { IdlEvent } from "../../anchor"; + +export class WormholeEventsCoder implements EventCoder { + constructor(_idl: Idl) {} + + decode>(_log: string): Event | null { + throw new Error("Wormhole program does not have events"); + } +} diff --git a/sdk/js/src/solana/wormhole/coder/index.ts b/sdk/js/src/solana/wormhole/coder/index.ts new file mode 100644 index 000000000..026a20a4a --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/index.ts @@ -0,0 +1,24 @@ +import { Coder, Idl } from "@project-serum/anchor"; +import { WormholeAccountsCoder } from "./accounts"; +import { WormholeEventsCoder } from "./events"; +import { WormholeInstructionCoder } from "./instruction"; +import { WormholeStateCoder } from "./state"; +import { WormholeTypesCoder } from "./types"; + +export { WormholeInstruction } from "./instruction"; + +export class WormholeCoder implements Coder { + readonly instruction: WormholeInstructionCoder; + readonly accounts: WormholeAccountsCoder; + readonly state: WormholeStateCoder; + readonly events: WormholeEventsCoder; + readonly types: WormholeTypesCoder; + + constructor(idl: Idl) { + this.instruction = new WormholeInstructionCoder(idl); + this.accounts = new WormholeAccountsCoder(idl); + this.state = new WormholeStateCoder(idl); + this.events = new WormholeEventsCoder(idl); + this.types = new WormholeTypesCoder(idl); + } +} diff --git a/sdk/js/src/solana/wormhole/coder/instruction.ts b/sdk/js/src/solana/wormhole/coder/instruction.ts new file mode 100644 index 000000000..2c3e3024c --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/instruction.ts @@ -0,0 +1,154 @@ +import { Idl, InstructionCoder } from "@project-serum/anchor"; +import { ETHEREUM_KEY_LENGTH } from "../../utils"; + +export class WormholeInstructionCoder implements InstructionCoder { + constructor(_: Idl) {} + + encode(ixName: string, ix: any): Buffer { + switch (ixName) { + case "initialize": { + return encodeInitialize(ix); + } + case "postVaa": { + return encodePostVaa(ix); + } + case "setFees": { + return encodeSetFees(ix); + } + case "transferFees": { + return encodeTransferFees(ix); + } + case "upgradeContract": { + return encodeUpgradeContract(ix); + } + case "upgradeGuardianSet": { + return encodeUpgradeGuardianSet(ix); + } + case "verifySignatures": { + return encodeVerifySignatures(ix); + } + default: { + throw new Error(`Invalid instruction: ${ixName}`); + } + } + } + + encodeState(_ixName: string, _ix: any): Buffer { + throw new Error("Wormhole program does not have state"); + } +} + +/** Solitaire enum of existing the Core Bridge's instructions. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/bridge/program/src/lib.rs#L92 + */ +export enum WormholeInstruction { + Initialize, + PostMessage, + PostVAA, + SetFees, + TransferFees, + UpgradeContract, + UpgradeGuardianSet, + VerifySignatures, + PostMessageUnreliable, // sounds useful +} + +function encodeInitialize({ + guardianSetExpirationTime, + fee, + initialGuardians, +}: any): Buffer { + if (typeof fee != "bigint") { + fee = BigInt(fee); + } + const initialGuardiansLen = initialGuardians.length; + const serialized = Buffer.alloc( + 16 + initialGuardiansLen * ETHEREUM_KEY_LENGTH + ); + serialized.writeUInt32LE(guardianSetExpirationTime, 0); + serialized.writeBigUInt64LE(fee, 4); + serialized.writeUInt32LE(initialGuardiansLen, 12); + for (let i = 0; i < initialGuardiansLen; ++i) { + const key = initialGuardians.at(i)!; + if (!Buffer.isBuffer(key)) { + throw new Error("key must be Buffer"); + } + serialized.write(key.toString("hex"), 16 + i * ETHEREUM_KEY_LENGTH, "hex"); + } + return encodeWormholeInstructionData( + WormholeInstruction.Initialize, + serialized + ); +} + +function encodeWormholeInstructionData( + instructionType: WormholeInstruction, + data?: Buffer +): Buffer { + const instructionData = Buffer.alloc( + 1 + (data === undefined ? 0 : data.length) + ); + instructionData.writeUInt8(instructionType, 0); + if (data !== undefined) { + instructionData.write(data.toString("hex"), 1, "hex"); + } + return instructionData; +} + +function encodePostVaa({ + version, + guardianSetIndex, + timestamp, + nonce, + emitterChain, + emitterAddress, + sequence, + consistencyLevel, + payload, +}: any) { + if (!Buffer.isBuffer(emitterAddress)) { + throw new Error("emitterAddress must be Buffer"); + } + if (!Buffer.isBuffer(payload)) { + throw new Error("payload must be Buffer"); + } + if (typeof sequence != "bigint") { + sequence = BigInt(sequence); + } + const serialized = Buffer.alloc(60 + payload.length); + serialized.writeUInt8(version, 0); + serialized.writeUInt32LE(guardianSetIndex, 1); + serialized.writeUInt32LE(timestamp, 5); + serialized.writeUInt32LE(nonce, 9); + serialized.writeUInt16LE(emitterChain, 13); + serialized.write(emitterAddress.toString("hex"), 15, "hex"); + serialized.writeBigInt64LE(sequence, 47); + serialized.writeUInt8(consistencyLevel, 55); + serialized.writeUInt32LE(payload.length, 56); + serialized.write(payload.toString("hex"), 60, "hex"); + return encodeWormholeInstructionData(WormholeInstruction.PostVAA, serialized); +} + +function encodeSetFees({}: any) { + return encodeWormholeInstructionData(WormholeInstruction.SetFees); +} + +function encodeTransferFees({}: any) { + return encodeWormholeInstructionData(WormholeInstruction.TransferFees); +} + +function encodeUpgradeContract({}: any) { + return encodeWormholeInstructionData(WormholeInstruction.UpgradeContract); +} + +function encodeUpgradeGuardianSet({}: any) { + return encodeWormholeInstructionData(WormholeInstruction.UpgradeGuardianSet); +} + +function encodeVerifySignatures({ signatureStatus }: any) { + return encodeWormholeInstructionData( + WormholeInstruction.VerifySignatures, + Buffer.from(signatureStatus, "hex") + ); +} diff --git a/sdk/js/src/solana/wormhole/coder/state.ts b/sdk/js/src/solana/wormhole/coder/state.ts new file mode 100644 index 000000000..99a27aa9b --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/state.ts @@ -0,0 +1,12 @@ +import { Idl, StateCoder } from "@project-serum/anchor"; + +export class WormholeStateCoder implements StateCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _account: T): Promise { + throw new Error("Wormhole program does not have state"); + } + decode(_ix: Buffer): T { + throw new Error("Wormhole program does not have state"); + } +} \ No newline at end of file diff --git a/sdk/js/src/solana/wormhole/coder/types.ts b/sdk/js/src/solana/wormhole/coder/types.ts new file mode 100644 index 000000000..3bc74c97c --- /dev/null +++ b/sdk/js/src/solana/wormhole/coder/types.ts @@ -0,0 +1,12 @@ +import { Idl, TypesCoder } from "@project-serum/anchor"; + +export class WormholeTypesCoder implements TypesCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _type: T): Buffer { + throw new Error("Wormhole program does not have user-defined types"); + } + decode(_name: string, _typeData: Buffer): T { + throw new Error("Wormhole program does not have user-defined types"); + } + } \ No newline at end of file diff --git a/sdk/js/src/solana/wormhole/cpi.ts b/sdk/js/src/solana/wormhole/cpi.ts new file mode 100644 index 000000000..24760c7bd --- /dev/null +++ b/sdk/js/src/solana/wormhole/cpi.ts @@ -0,0 +1,92 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; +import { + deriveEmitterSequenceKey, + deriveFeeCollectorKey, + deriveWormholeEmitterKey, + deriveWormholeBridgeDataKey, + getEmitterKeys, +} from "./accounts"; +import { getPostMessageAccounts } from "./instructions"; + +export interface WormholeDerivedAccounts { + /** + * seeds = ["Bridge"], seeds::program = wormholeProgram + */ + wormholeBridge: PublicKey; + /** + * seeds = ["emitter"], seeds::program = cpiProgramId + */ + wormholeEmitter: PublicKey; + /** + * seeds = ["Sequence", wormholeEmitter], seeds::program = wormholeProgram + */ + wormholeSequence: PublicKey; + /** + * seeds = ["fee_collector"], seeds::program = wormholeProgram + */ + wormholeFeeCollector: PublicKey; +} + +/** + * Generate Wormhole PDAs. + * + * @param cpiProgramId + * @param wormholeProgramId + * @returns + */ +export function getWormholeDerivedAccounts( + cpiProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData +): WormholeDerivedAccounts { + const { emitter: wormholeEmitter, sequence: wormholeSequence } = + getEmitterKeys(cpiProgramId, wormholeProgramId); + return { + wormholeBridge: deriveWormholeBridgeDataKey(wormholeProgramId), + wormholeEmitter, + wormholeSequence, + wormholeFeeCollector: deriveFeeCollectorKey(wormholeProgramId), + }; +} + +export interface PostMessageCpiAccounts extends WormholeDerivedAccounts { + payer: PublicKey; + wormholeMessage: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +/** + * Generate accounts needed to perform `post_message` instruction + * as cross-program invocation. + * + * @param cpiProgramId + * @param wormholeProgramId + * @param payer + * @param message + * @returns + */ +export function getPostMessageCpiAccounts( + cpiProgramId: PublicKeyInitData, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + message: PublicKeyInitData +): PostMessageCpiAccounts { + const accounts = getPostMessageAccounts( + wormholeProgramId, + payer, + cpiProgramId, + message + ); + return { + payer: accounts.payer, + wormholeBridge: accounts.bridge, + wormholeMessage: accounts.message, + wormholeEmitter: accounts.emitter, + wormholeSequence: accounts.sequence, + wormholeFeeCollector: accounts.feeCollector, + clock: accounts.clock, + rent: accounts.rent, + systemProgram: accounts.systemProgram, + }; +} diff --git a/sdk/js/src/solana/wormhole/index.ts b/sdk/js/src/solana/wormhole/index.ts new file mode 100644 index 000000000..694b578d5 --- /dev/null +++ b/sdk/js/src/solana/wormhole/index.ts @@ -0,0 +1,5 @@ +export * from "./accounts"; +export * from "./cpi"; +export * from "./instructions"; +export * from "./message"; +export * from "./program"; diff --git a/sdk/js/src/solana/wormhole/instructions/feeTransfer.ts b/sdk/js/src/solana/wormhole/instructions/feeTransfer.ts new file mode 100644 index 000000000..5ce8e9077 --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/feeTransfer.ts @@ -0,0 +1,27 @@ +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { deriveFeeCollectorKey, getWormholeBridgeData } from "../accounts"; + +export async function createBridgeFeeTransferInstruction( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + commitment?: Commitment +): Promise { + const fee = await getWormholeBridgeData( + connection, + wormholeProgramId, + commitment + ).then((data) => data.config.fee); + return SystemProgram.transfer({ + fromPubkey: new PublicKey(payer), + toPubkey: deriveFeeCollectorKey(wormholeProgramId), + lamports: fee, + }); +} diff --git a/sdk/js/src/solana/wormhole/instructions/governance.ts b/sdk/js/src/solana/wormhole/instructions/governance.ts new file mode 100644 index 000000000..95ee57b28 --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/governance.ts @@ -0,0 +1,262 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { + isBytes, + ParsedGovernanceVaa, + parseGovernanceVaa, + SignedVaa, +} from "../../../vaa"; +import { createReadOnlyWormholeProgramInterface } from "../program"; +import { + deriveWormholeBridgeDataKey, + deriveClaimKey, + deriveFeeCollectorKey, + deriveGuardianSetKey, + derivePostedVaaKey, + deriveUpgradeAuthorityKey, +} from "../accounts"; +import { BpfLoaderUpgradeable, deriveUpgradeableProgramKey } from "../../utils"; + +export function createSetFeesInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): TransactionInstruction { + const methods = + createReadOnlyWormholeProgramInterface(wormholeProgramId).methods.setFees(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getSetFeesAccounts(wormholeProgramId, payer, vaa) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface SetFeesAccounts { + payer: PublicKey; + bridge: PublicKey; + vaa: PublicKey; + claim: PublicKey; + systemProgram: PublicKey; +} + +export function getSetFeesAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): SetFeesAccounts { + const parsed = isBytes(vaa) ? parseGovernanceVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + wormholeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + systemProgram: SystemProgram.programId, + }; +} + +export function createTransferFeesInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + recipient: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): TransactionInstruction { + const methods = + createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.transferFees(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getTransferFeesAccounts( + wormholeProgramId, + payer, + recipient, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface TransferFeesAccounts { + payer: PublicKey; + bridge: PublicKey; + vaa: PublicKey; + claim: PublicKey; + feeCollector: PublicKey; + recipient: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getTransferFeesAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + recipient: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): TransferFeesAccounts { + const parsed = isBytes(vaa) ? parseGovernanceVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + wormholeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + feeCollector: deriveFeeCollectorKey(wormholeProgramId), + recipient: new PublicKey(recipient), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} + +export function createUpgradeGuardianSetInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): TransactionInstruction { + const methods = + createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.upgradeGuardianSet(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getUpgradeGuardianSetAccounts( + wormholeProgramId, + payer, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface UpgradeGuardianSetAccounts { + payer: PublicKey; + bridge: PublicKey; + vaa: PublicKey; + claim: PublicKey; + guardianSetOld: PublicKey; + guardianSetNew: PublicKey; + systemProgram: PublicKey; +} + +export function getUpgradeGuardianSetAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): UpgradeGuardianSetAccounts { + const parsed = isBytes(vaa) ? parseGovernanceVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + wormholeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + guardianSetOld: deriveGuardianSetKey( + wormholeProgramId, + parsed.guardianSetIndex + ), + guardianSetNew: deriveGuardianSetKey( + wormholeProgramId, + parsed.guardianSetIndex + 1 + ), + systemProgram: SystemProgram.programId, + }; +} + +export function createUpgradeContractInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa +): TransactionInstruction { + const methods = + createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.upgradeContract(); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getUpgradeContractAccounts(wormholeProgramId, payer, vaa) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface UpgradeContractAccounts { + payer: PublicKey; + bridge: PublicKey; + vaa: PublicKey; + claim: PublicKey; + upgradeAuthority: PublicKey; + spill: PublicKey; + implementation: PublicKey; + programData: PublicKey; + wormholeProgram: PublicKey; + rent: PublicKey; + clock: PublicKey; + bpfLoaderUpgradeable: PublicKey; + systemProgram: PublicKey; +} + +export function getUpgradeContractAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedGovernanceVaa, + spill?: PublicKeyInitData +): UpgradeContractAccounts { + const parsed = isBytes(vaa) ? parseGovernanceVaa(vaa) : vaa; + const implementation = parsed.orderPayload; + if (implementation.length != 32) { + throw new Error("implementation.length != 32"); + } + return { + payer: new PublicKey(payer), + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + claim: deriveClaimKey( + wormholeProgramId, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ), + upgradeAuthority: deriveUpgradeAuthorityKey(wormholeProgramId), + spill: new PublicKey(spill === undefined ? payer : spill), + implementation: new PublicKey(implementation), + programData: deriveUpgradeableProgramKey(wormholeProgramId), + wormholeProgram: new PublicKey(wormholeProgramId), + rent: SYSVAR_RENT_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + bpfLoaderUpgradeable: BpfLoaderUpgradeable.programId, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/wormhole/instructions/index.ts b/sdk/js/src/solana/wormhole/instructions/index.ts new file mode 100644 index 000000000..5ed004572 --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/index.ts @@ -0,0 +1,6 @@ +export * from "./feeTransfer"; +export * from "./governance"; +export * from "./initialize"; +export * from "./postMessage"; +export * from "./postVaa"; +export * from "./verifySignature"; diff --git a/sdk/js/src/solana/wormhole/instructions/initialize.ts b/sdk/js/src/solana/wormhole/instructions/initialize.ts new file mode 100644 index 000000000..639287b1b --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/initialize.ts @@ -0,0 +1,64 @@ +import { + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createReadOnlyWormholeProgramInterface } from "../program"; +import { + deriveFeeCollectorKey, + deriveGuardianSetKey, + deriveWormholeBridgeDataKey, +} from "../accounts"; + +export function createInitializeInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + guardianSetExpirationTime: number, + fee: bigint, + initialGuardians: Buffer[] +): TransactionInstruction { + const methods = createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.initialize( + guardianSetExpirationTime, + fee as any, + initialGuardians as any + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getInitializeAccounts(wormholeProgramId, payer) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface InitializeAccounts { + bridge: PublicKey; + guardianSet: PublicKey; + feeCollector: PublicKey; + payer: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getInitializeAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData +): InitializeAccounts { + return { + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + guardianSet: deriveGuardianSetKey(wormholeProgramId, 0), + feeCollector: deriveFeeCollectorKey(wormholeProgramId), + payer: new PublicKey(payer), + clock: SYSVAR_CLOCK_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/wormhole/instructions/postMessage.ts b/sdk/js/src/solana/wormhole/instructions/postMessage.ts new file mode 100644 index 000000000..676f86bec --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/postMessage.ts @@ -0,0 +1,48 @@ +import { + PublicKey, + PublicKeyInitData, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from "@solana/web3.js"; +import { + deriveWormholeBridgeDataKey, + deriveFeeCollectorKey, + getEmitterKeys, +} from "../accounts"; + +/** All accounts required to make a cross-program invocation with the Core Bridge program */ +export interface PostMessageAccounts { + bridge: PublicKey; + message: PublicKey; + emitter: PublicKey; + sequence: PublicKey; + payer: PublicKey; + feeCollector: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getPostMessageAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + emitterProgramId: PublicKeyInitData, + message: PublicKeyInitData +): PostMessageAccounts { + const { emitter, sequence } = getEmitterKeys( + emitterProgramId, + wormholeProgramId + ); + return { + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + message: new PublicKey(message), + emitter, + sequence, + payer: new PublicKey(payer), + feeCollector: deriveFeeCollectorKey(wormholeProgramId), + clock: SYSVAR_CLOCK_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/wormhole/instructions/postVaa.ts b/sdk/js/src/solana/wormhole/instructions/postVaa.ts new file mode 100644 index 000000000..eca8bcea3 --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/postVaa.ts @@ -0,0 +1,98 @@ +import { + PublicKey, + PublicKeyInitData, + TransactionInstruction, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from "@solana/web3.js"; +import { createReadOnlyWormholeProgramInterface } from "../program"; +import { + deriveWormholeBridgeDataKey, + deriveGuardianSetKey, + derivePostedVaaKey, +} from "../accounts"; +import { isBytes, ParsedVaa, parseVaa, SignedVaa } from "../../../vaa"; + +/** + * Make {@link TransactionInstruction} for `post_vaa` instruction. + * + * This is used in {@link createPostSignedVaaTransactions}'s last transaction. + * `signatureSet` is a {@link web3.Keypair} generated outside of this method, which was used + * to write signatures and the message hash to. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/bridge/program/src/api/post_vaa.rs + * + * @param {PublicKeyInitData} wormholeProgramId - wormhole program address + * @param {PublicKeyInitData} payer - transaction signer address + * @param {SignedVaa | ParsedVaa} vaa - either signed VAA bytes or parsed VAA (use {@link parseVaa} on signed VAA) + * @param {PublicKeyInitData} signatureSet - key for signature set account + */ +export function createPostVaaInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa, + signatureSet: PublicKeyInitData +): TransactionInstruction { + const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa; + const methods = createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.postVaa( + parsed.version, + parsed.guardianSetIndex, + parsed.timestamp, + parsed.nonce, + parsed.emitterChain, + parsed.emitterAddress as any, + parsed.sequence as any, + parsed.consistencyLevel, + parsed.payload + ); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getPostVaaAccounts( + wormholeProgramId, + payer, + signatureSet, + parsed + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface PostVaaAccounts { + guardianSet: PublicKey; + bridge: PublicKey; + signatureSet: PublicKey; + vaa: PublicKey; + payer: PublicKey; + clock: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getPostVaaAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + signatureSet: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa +): PostVaaAccounts { + const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa; + return { + guardianSet: deriveGuardianSetKey( + wormholeProgramId, + parsed.guardianSetIndex + ), + bridge: deriveWormholeBridgeDataKey(wormholeProgramId), + signatureSet: new PublicKey(signatureSet), + vaa: derivePostedVaaKey(wormholeProgramId, parsed.hash), + payer: new PublicKey(payer), + clock: SYSVAR_CLOCK_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/wormhole/instructions/verifySignature.ts b/sdk/js/src/solana/wormhole/instructions/verifySignature.ts new file mode 100644 index 000000000..3a2ae478e --- /dev/null +++ b/sdk/js/src/solana/wormhole/instructions/verifySignature.ts @@ -0,0 +1,172 @@ +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { createSecp256k1Instruction } from "../../utils"; +import { + getGuardianSet, + deriveGuardianSetKey, + getWormholeBridgeData, +} from "../accounts"; +import { isBytes, ParsedVaa, parseVaa, SignedVaa } from "../../../vaa"; +import { createReadOnlyWormholeProgramInterface } from "../program"; + +const MAX_LEN_GUARDIAN_KEYS = 19; + +/** + * This is used in {@link createPostSignedVaaTransactions}'s initial transactions. + * + * Signatures are batched in groups of 7 due to instruction + * data limits. These signatures are passed through to the Secp256k1 + * program to verify that the guardian public keys can be recovered. + * This instruction is paired with `verify_signatures` to validate the + * pubkey recovery. + * + * There are at most three pairs of instructions created. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/bridge/program/src/api/verify_signature.rs + * + * + * @param {Connection} connection - Solana web3 connection + * @param {PublicKeyInitData} wormholeProgramId - wormhole program address + * @param {PublicKeyInitData} payer - transaction signer address + * @param {SignedVaa | ParsedVaa} vaa - either signed VAA bytes or parsed VAA (use {@link parseVaa} on signed VAA) + * @param {PublicKeyInitData} signatureSet - address to account of verified signatures + * @param {web3.ConfirmOptions} [options] - Solana confirmation options + */ +export async function createVerifySignaturesInstructions( + connection: Connection, + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa, + signatureSet: PublicKeyInitData, + commitment?: Commitment +): Promise { + const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa; + const guardianSetIndex = parsed.guardianSetIndex; + const info = await getWormholeBridgeData(connection, wormholeProgramId); + if (guardianSetIndex != info.guardianSetIndex) { + throw new Error("guardianSetIndex != config.guardianSetIndex"); + } + + const guardianSetData = await getGuardianSet( + connection, + wormholeProgramId, + guardianSetIndex, + commitment + ); + + const guardianSignatures = parsed.guardianSignatures; + const guardianKeys = guardianSetData.keys; + + const batchSize = 7; + const instructions: TransactionInstruction[] = []; + for (let i = 0; i < Math.ceil(guardianSignatures.length / batchSize); ++i) { + const start = i * batchSize; + const end = Math.min(guardianSignatures.length, (i + 1) * batchSize); + + const signatureStatus = Buffer.alloc(MAX_LEN_GUARDIAN_KEYS, -1); + const signatures: Buffer[] = []; + const keys: Buffer[] = []; + for (let j = 0; j < end - start; ++j) { + const item = guardianSignatures.at(j + start)!; + signatures.push(item.signature); + + const key = guardianKeys.at(item.index)!; + keys.push(key); + + signatureStatus.writeInt8(j, item.index); + } + + instructions.push( + createSecp256k1Instruction(signatures, keys, parsed.hash) + ); + instructions.push( + createVerifySignaturesInstruction( + wormholeProgramId, + payer, + parsed, + signatureSet, + signatureStatus + ) + ); + } + return instructions; +} + +/** + * Make {@link TransactionInstruction} for `verify_signatures` instruction. + * + * This is used in {@link createVerifySignaturesInstructions} for each batch of signatures being verified. + * `signatureSet` is a {@link web3.Keypair} generated outside of this method, used + * for writing signatures and the message hash to. + * + * https://github.com/certusone/wormhole/blob/dev.v2/solana/bridge/program/src/api/verify_signature.rs + * + * @param {PublicKeyInitData} wormholeProgramId - wormhole program address + * @param {PublicKeyInitData} payer - transaction signer address + * @param {SignedVaa | ParsedVaa} vaa - either signed VAA (Buffer) or parsed VAA (use {@link parseVaa} on signed VAA) + * @param {PublicKeyInitData} signatureSet - key for signature set account + * @param {Buffer} signatureStatus - array of guardian indices + * + */ +function createVerifySignaturesInstruction( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa, + signatureSet: PublicKeyInitData, + signatureStatus: Buffer +): TransactionInstruction { + const methods = createReadOnlyWormholeProgramInterface( + wormholeProgramId + ).methods.verifySignatures(signatureStatus as any); + + // @ts-ignore + return methods._ixFn(...methods._args, { + accounts: getVerifySignatureAccounts( + wormholeProgramId, + payer, + signatureSet, + vaa + ) as any, + signers: undefined, + remainingAccounts: undefined, + preInstructions: undefined, + postInstructions: undefined, + }); +} + +export interface VerifySignatureAccounts { + payer: PublicKey; + guardianSet: PublicKey; + signatureSet: PublicKey; + instructions: PublicKey; + rent: PublicKey; + systemProgram: PublicKey; +} + +export function getVerifySignatureAccounts( + wormholeProgramId: PublicKeyInitData, + payer: PublicKeyInitData, + signatureSet: PublicKeyInitData, + vaa: SignedVaa | ParsedVaa +): VerifySignatureAccounts { + const parsed = isBytes(vaa) ? parseVaa(vaa) : vaa; + return { + payer: new PublicKey(payer), + guardianSet: deriveGuardianSetKey( + wormholeProgramId, + parsed.guardianSetIndex + ), + signatureSet: new PublicKey(signatureSet), + instructions: SYSVAR_INSTRUCTIONS_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; +} diff --git a/sdk/js/src/solana/wormhole/message.ts b/sdk/js/src/solana/wormhole/message.ts new file mode 100644 index 000000000..554605616 --- /dev/null +++ b/sdk/js/src/solana/wormhole/message.ts @@ -0,0 +1,66 @@ +import { PublicKey, PublicKeyInitData } from "@solana/web3.js"; + +export class MessageData { + vaaVersion: number; + consistencyLevel: number; + vaaTime: number; + vaaSignatureAccount: PublicKey; + submissionTime: number; + nonce: number; + sequence: bigint; + emitterChain: number; + emitterAddress: Buffer; + payload: Buffer; + + constructor( + vaaVersion: number, + consistencyLevel: number, + vaaTime: number, + vaaSignatureAccount: PublicKeyInitData, + submissionTime: number, + nonce: number, + sequence: bigint, + emitterChain: number, + emitterAddress: Buffer, + payload: Buffer + ) { + this.vaaVersion = vaaVersion; + this.consistencyLevel = consistencyLevel; + this.vaaTime = vaaTime; + this.vaaSignatureAccount = new PublicKey(vaaSignatureAccount); + this.submissionTime = submissionTime; + this.nonce = nonce; + this.sequence = sequence; + this.emitterChain = emitterChain; + this.emitterAddress = emitterAddress; + this.payload = payload; + } + + static deserialize(data: Buffer): MessageData { + const vaaVersion = data.readUInt8(0); + const consistencyLevel = data.readUInt8(1); + const vaaTime = data.readUInt32LE(2); + const vaaSignatureAccount = new PublicKey(data.subarray(6, 38)); + const submissionTime = data.readUInt32LE(38); + const nonce = data.readUInt32LE(42); + const sequence = data.readBigUInt64LE(46); + const emitterChain = data.readUInt16LE(54); + const emitterAddress = data.subarray(56, 88); + // unnecessary to get Vec length, but being explicit in borsh deserialization + const payloadLen = data.readUInt32LE(88); + const payload = data.subarray(92, 92 + payloadLen); + + return new MessageData( + vaaVersion, + consistencyLevel, + vaaTime, + vaaSignatureAccount, + submissionTime, + nonce, + sequence, + emitterChain, + emitterAddress, + payload + ); + } +} diff --git a/sdk/js/src/solana/wormhole/program.ts b/sdk/js/src/solana/wormhole/program.ts new file mode 100644 index 000000000..e60139d80 --- /dev/null +++ b/sdk/js/src/solana/wormhole/program.ts @@ -0,0 +1,40 @@ +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { Program, Provider } from "@project-serum/anchor"; +import { createReadOnlyProvider } from "../utils"; +import { WormholeCoder } from "./coder"; +import { Wormhole } from "../types/wormhole"; + +import IDL from "../../anchor-idl/wormhole.json"; + +export function createWormholeProgramInterface( + programId: PublicKeyInitData, + provider?: Provider +): Program { + return new Program( + IDL as Wormhole, + new PublicKey(programId), + provider === undefined ? ({ connection: null } as any) : provider, + coder() + ); +} + +export function createReadOnlyWormholeProgramInterface( + programId: PublicKeyInitData, + connection?: Connection +): Program { + return createWormholeProgramInterface( + programId, + createReadOnlyProvider(connection) + ); +} + +export function coder(): WormholeCoder { + return new WormholeCoder(IDL as Wormhole); +} diff --git a/sdk/js/src/token_bridge/__tests__/algorand-integration.ts b/sdk/js/src/token_bridge/__tests__/algorand-integration.ts index c6f0aaca1..da2531301 100644 --- a/sdk/js/src/token_bridge/__tests__/algorand-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/algorand-integration.ts @@ -32,13 +32,13 @@ import { redeemOnAlgorand, redeemOnEth, textToUint8Array, - TokenImplementation__factory, transferFromAlgorand, transferFromEth, uint8ArrayToHex, updateWrappedOnEth, WormholeWrappedInfo, } from "../.."; +import { TokenImplementation__factory } from "../../ethers-contracts"; import { _parseVAAAlgorand } from "../../algorand"; import { createAsset, @@ -50,7 +50,6 @@ import { signSendAndConfirmAlgorand, } from "../../algorand/__tests__/testHelpers"; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { setDefaultWasm } from "../../solana/wasm"; import { safeBigIntToNumber } from "../../utils/bigint"; import { ETH_NODE_URL, @@ -62,8 +61,6 @@ import { const CORE_ID = BigInt(4); const TOKEN_BRIDGE_ID = BigInt(6); -setDefaultWasm("node"); - jest.setTimeout(60000); describe("Algorand tests", () => { diff --git a/sdk/js/src/token_bridge/__tests__/aptos-integration.ts b/sdk/js/src/token_bridge/__tests__/aptos-integration.ts index 5ca11a7b0..89b536ac8 100644 --- a/sdk/js/src/token_bridge/__tests__/aptos-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/aptos-integration.ts @@ -32,14 +32,12 @@ import { redeemOnEth, generateSignAndSubmitEntryFunction, generateSignAndSubmitScript, - TokenImplementation__factory, transferFromAptos, transferFromEth, tryNativeToHexString, tryNativeToUint8Array, uint8ArrayToHex, } from "../.."; -import { setDefaultWasm } from "../../solana/wasm"; import { APTOS_FAUCET_URL, APTOS_NODE_URL, @@ -57,8 +55,7 @@ import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport" import { ethers } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { registerCoin } from "../../aptos"; - -setDefaultWasm("node"); +import { TokenImplementation__factory } from "../../ethers-contracts"; const JEST_TEST_TIMEOUT = 60000; jest.setTimeout(JEST_TEST_TIMEOUT); @@ -90,6 +87,7 @@ describe("Aptos SDK tests", () => { sender, attestPayload )) as Types.UserTransaction; + await client.waitForTransaction(tx.hash); // get signed attest vaa let sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx); @@ -148,6 +146,7 @@ describe("Aptos SDK tests", () => { sender, transferPayload )) as Types.UserTransaction; + await client.waitForTransaction(tx.hash); const balanceAfterTransferAptos = ethers.BigNumber.from( await getBalanceAptos(client, COIN_TYPE, sender.address()) ); @@ -251,11 +250,12 @@ describe("Aptos SDK tests", () => { attestVAA ); try { - await generateSignAndSubmitEntryFunction( + const tx = await generateSignAndSubmitEntryFunction( client, recipient, createWrappedCoinTypePayload ); + await client.waitForTransaction(tx.hash); } catch (e) { // only throw if token has not been attested but this call fails if ( @@ -273,11 +273,12 @@ describe("Aptos SDK tests", () => { attestVAA ); try { - await generateSignAndSubmitEntryFunction( + const tx = await generateSignAndSubmitEntryFunction( client, recipient, createWrappedCoinPayload ); + await client.waitForTransaction(tx.hash); } catch (e) { // only throw if token has not been attested but this call fails if ( @@ -311,11 +312,7 @@ describe("Aptos SDK tests", () => { ); expect(info.chainId).toEqual(CHAIN_ID_ETH); expect(info.isWrapped).toEqual( - await getIsWrappedAssetAptos( - client, - aptosTokenBridge, - aptosWrappedType - ) + await getIsWrappedAssetAptos(client, aptosTokenBridge, aptosWrappedType) ); // transfer from eth @@ -347,7 +344,7 @@ describe("Aptos SDK tests", () => { 5 ); expect(transferVAA).toBeTruthy(); - + // register token on aptos const script = registerCoin(aptosTokenBridge, CHAIN_ID_ETH, TEST_ERC20); await generateSignAndSubmitScript(client, recipient, script); @@ -361,7 +358,12 @@ describe("Aptos SDK tests", () => { aptosTokenBridge, transferVAA ); - await generateSignAndSubmitEntryFunction(client, recipient, redeemPayload); + const tx = await generateSignAndSubmitEntryFunction( + client, + recipient, + redeemPayload + ); + await client.waitForTransaction(tx.hash); expect( await getIsTransferCompletedAptos(client, aptosTokenBridge, transferVAA) ).toBe(true); diff --git a/sdk/js/src/token_bridge/__tests__/eth-integration.ts b/sdk/js/src/token_bridge/__tests__/eth-integration.ts index 6925c989b..4ff6a65bc 100644 --- a/sdk/js/src/token_bridge/__tests__/eth-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/eth-integration.ts @@ -3,7 +3,8 @@ import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport" import { describe, expect, jest, test } from "@jest/globals"; import { ASSOCIATED_TOKEN_PROGRAM_ID, - Token, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddress, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { @@ -24,18 +25,15 @@ import { getEmitterAddressEth, getForeignAssetSolana, getIsTransferCompletedSolana, - hexToUint8Array, - nativeToHexString, parseSequenceFromLogEth, postVaaSolana, redeemOnSolana, - TokenImplementation__factory, transferFromEth, tryNativeToUint8Array, } from "../.."; +import { TokenImplementation__factory } from "../../ethers-contracts"; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { postVaaWithRetry } from "../../solana/postVaa"; -import { setDefaultWasm } from "../../solana/wasm"; +import { postVaaWithRetry } from "../../solana/sendAndConfirmPostVaa"; import { ETH_NODE_URL, ETH_PRIVATE_KEY, @@ -45,8 +43,6 @@ import { WORMHOLE_RPC_HOSTS, } from "./consts"; -setDefaultWasm("node"); - jest.setTimeout(60000); async function transferFromEthToSolana(): Promise { @@ -59,12 +55,10 @@ async function transferFromEthToSolana(): Promise { connection, CONTRACTS.DEVNET.solana.token_bridge, CHAIN_ID_ETH, - hexToUint8Array(nativeToHexString(TEST_ERC20, CHAIN_ID_ETH) || "") + tryNativeToUint8Array(TEST_ERC20, CHAIN_ID_ETH) )) || "" ); - const recipient = await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, + const recipient = await getAssociatedTokenAddress( solanaMintKey, keypair.publicKey ); @@ -72,16 +66,14 @@ async function transferFromEthToSolana(): Promise { const associatedAddressInfo = await connection.getAccountInfo(recipient); if (!associatedAddressInfo) { const transaction = new Transaction().add( - await Token.createAssociatedTokenAccountInstruction( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - solanaMintKey, + await createAssociatedTokenAccountInstruction( + keypair.publicKey, // payer recipient, keypair.publicKey, // owner - keypair.publicKey // payer + solanaMintKey ) ); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = keypair.publicKey; // sign, send, and confirm transaction @@ -107,9 +99,7 @@ async function transferFromEthToSolana(): Promise { TEST_ERC20, amount, CHAIN_ID_SOLANA, - hexToUint8Array( - nativeToHexString(recipient.toString(), CHAIN_ID_SOLANA) || "" - ) + tryNativeToUint8Array(recipient.toString(), CHAIN_ID_SOLANA) ); // get the sequence from the logs (needed to fetch the vaa) const sequence = await parseSequenceFromLogEth( @@ -220,9 +210,7 @@ describe("Ethereum to Solana and Back", () => { tryNativeToUint8Array(TEST_ERC20, CHAIN_ID_ETH) ); const solanaMintKey = new PublicKey(SolanaForeignAsset || ""); - const recipient = await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, + const recipient = await getAssociatedTokenAddress( solanaMintKey, keypair.publicKey ); @@ -232,16 +220,14 @@ describe("Ethereum to Solana and Back", () => { ); if (!associatedAddressInfo) { const transaction = new Transaction().add( - await Token.createAssociatedTokenAccountInstruction( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - solanaMintKey, + await createAssociatedTokenAccountInstruction( + keypair.publicKey, // payer recipient, keypair.publicKey, // owner - keypair.publicKey // payer + solanaMintKey ) ); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = keypair.publicKey; // sign, send, and confirm transaction diff --git a/sdk/js/src/token_bridge/__tests__/near-integration.ts b/sdk/js/src/token_bridge/__tests__/near-integration.ts index daee24cba..68e83cb16 100644 --- a/sdk/js/src/token_bridge/__tests__/near-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/near-integration.ts @@ -15,7 +15,6 @@ import { redeemOnEth, redeemOnNear, registerAccount, - setDefaultWasm, tryNativeToUint8Array, updateWrappedOnEth, } from "../.."; @@ -37,8 +36,6 @@ import { } from "near-api-js/lib/providers"; import { parseNearAmount } from "near-api-js/lib/utils/format"; -setDefaultWasm("node"); - jest.setTimeout(60000); let near: Near; diff --git a/sdk/js/src/token_bridge/__tests__/solana-integration.ts b/sdk/js/src/token_bridge/__tests__/solana-integration.ts index a85f0cc62..53eed4171 100644 --- a/sdk/js/src/token_bridge/__tests__/solana-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/solana-integration.ts @@ -3,7 +3,7 @@ import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport" import { describe, expect, jest, test } from "@jest/globals"; import { ASSOCIATED_TOKEN_PROGRAM_ID, - Token, + getAssociatedTokenAddress, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { @@ -25,13 +25,12 @@ import { hexToUint8Array, parseSequenceFromLogSolana, redeemOnEth, - TokenImplementation__factory, transferFromSolana, tryNativeToHexString, tryNativeToUint8Array, } from "../.."; +import { TokenImplementation__factory } from "../../ethers-contracts"; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { setDefaultWasm } from "../../solana/wasm"; import { ETH_NODE_URL, ETH_PRIVATE_KEY3, @@ -41,8 +40,6 @@ import { WORMHOLE_RPC_HOSTS, } from "./consts"; -setDefaultWasm("node"); - jest.setTimeout(60000); describe("Solana to Ethereum", () => { @@ -134,9 +131,7 @@ describe("Solana to Ethereum", () => { const payerAddress = keypair.publicKey.toString(); // find the associated token account const fromAddress = ( - await Token.getAssociatedTokenAddress( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, + await getAssociatedTokenAddress( new PublicKey(TEST_SOLANA_TOKEN), keypair.publicKey ) diff --git a/sdk/js/src/token_bridge/__tests__/terra-integration.ts b/sdk/js/src/token_bridge/__tests__/terra-integration.ts index f188d644c..fab94cbac 100644 --- a/sdk/js/src/token_bridge/__tests__/terra-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/terra-integration.ts @@ -27,15 +27,14 @@ import { parseSequenceFromLogTerra, redeemOnEth, redeemOnTerra, - TokenImplementation__factory, transferFromEth, transferFromTerra, tryNativeToHexString, tryNativeToUint8Array, updateWrappedOnEth, } from "../.."; +import { TokenImplementation__factory } from "../../ethers-contracts"; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { setDefaultWasm } from "../../solana/wasm"; import { ETH_NODE_URL, ETH_PRIVATE_KEY4, @@ -53,8 +52,6 @@ import { waitForTerraExecution, } from "./helpers"; -setDefaultWasm("node"); - jest.setTimeout(60000); describe("Terra Integration Tests", () => { @@ -122,7 +119,9 @@ describe("Terra Integration Tests", () => { }, }, recipient_chain: CHAIN_ID_ETH, - recipient: Buffer.from(tryNativeToUint8Array(await signer.getAddress(), 'ethereum')).toString('base64'), + recipient: Buffer.from( + tryNativeToUint8Array(await signer.getAddress(), "ethereum") + ).toString("base64"), fee: "0", nonce: Math.round(Math.round(Math.random() * 100000)), }, diff --git a/sdk/js/src/token_bridge/attest.ts b/sdk/js/src/token_bridge/attest.ts index e4332b5c4..d5360f4d7 100644 --- a/sdk/js/src/token_bridge/attest.ts +++ b/sdk/js/src/token_bridge/attest.ts @@ -1,4 +1,11 @@ -import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { + Commitment, + Connection, + Keypair, + PublicKey, + PublicKeyInitData, + Transaction, +} from "@solana/web3.js"; import { MsgExecuteContract } from "@terra-money/terra.js"; import { MsgExecuteContract as MsgExecuteContractInjective } from "@injectivelabs/sdk-ts"; import { @@ -11,13 +18,14 @@ import { OnApplicationComplete, SuggestedParams, } from "algosdk"; +import { Account as nearAccount } from "near-api-js"; import BN from "bn.js"; import { ethers, PayableOverrides } from "ethers"; -import { isNativeDenom } from ".."; +import { isNativeDenom } from "../terra"; import { getMessageFee, optin, TransactionSignerPair } from "../algorand"; import { Bridge__factory } from "../ethers-contracts"; -import { getBridgeFeeIx, ixFromRust } from "../solana"; -import { importTokenWasm } from "../solana/wasm"; +import { createBridgeFeeTransferInstruction } from "../solana"; +import { createAttestTokenInstruction } from "../solana/tokenBridge"; import { callFunctionNear, hashAccount, @@ -131,31 +139,29 @@ export function attestFromXpla( export async function attestFromSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - mintAddress: string + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + commitment?: Commitment ): Promise { const nonce = createNonce().readUInt32LE(0); - const transferIx = await getBridgeFeeIx( + const transferIx = await createBridgeFeeTransferInstruction( connection, bridgeAddress, payerAddress ); - const { attest_ix } = await importTokenWasm(); const messageKey = Keypair.generate(); - const ix = ixFromRust( - attest_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - messageKey.publicKey.toString(), - mintAddress, - nonce - ) + const attestIx = createAttestTokenInstruction( + tokenBridgeAddress, + bridgeAddress, + payerAddress, + mintAddress, + messageKey.publicKey, + nonce ); - const transaction = new Transaction().add(transferIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); + const transaction = new Transaction().add(transferIx, attestIx); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); transaction.partialSign(messageKey); diff --git a/sdk/js/src/token_bridge/createWrapped.ts b/sdk/js/src/token_bridge/createWrapped.ts index 24f183abf..e1a4294ed 100644 --- a/sdk/js/src/token_bridge/createWrapped.ts +++ b/sdk/js/src/token_bridge/createWrapped.ts @@ -1,14 +1,22 @@ -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, + Transaction, +} from "@solana/web3.js"; import { MsgExecuteContract } from "@terra-money/terra.js"; import { Algodv2 } from "algosdk"; import { Types } from "aptos"; import BN from "bn.js"; import { ethers, Overrides } from "ethers"; import { fromUint8Array } from "js-base64"; -import { TransactionSignerPair, _parseVAAAlgorand, _submitVAAAlgorand } from "../algorand"; +import { + TransactionSignerPair, + _parseVAAAlgorand, + _submitVAAAlgorand, +} from "../algorand"; import { Bridge__factory } from "../ethers-contracts"; -import { ixFromRust } from "../solana"; -import { importTokenWasm } from "../solana/wasm"; import { submitVAAOnInjective } from "./redeem"; import { FunctionCallOptions } from "near-api-js/lib/account"; import { Provider } from "near-api-js/lib/providers"; @@ -18,6 +26,8 @@ import { createWrappedCoin as createWrappedCoinAptos, createWrappedCoinType as createWrappedCoinTypeAptos, } from "../aptos"; +import { createCreateWrappedInstruction } from "../solana/tokenBridge"; +import { SignedVaa } from "../vaa"; export async function createWrappedOnEth( tokenBridgeAddress: string, @@ -59,17 +69,21 @@ export function createWrappedOnXpla( export async function createWrappedOnSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - signedVAA: Uint8Array + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + signedVaa: SignedVaa, + commitment?: Commitment ): Promise { - const { create_wrapped_ix } = await importTokenWasm(); - const ix = ixFromRust( - create_wrapped_ix(tokenBridgeAddress, bridgeAddress, payerAddress, signedVAA) + const transaction = new Transaction().add( + createCreateWrappedInstruction( + tokenBridgeAddress, + bridgeAddress, + payerAddress, + signedVaa + ) ); - const transaction = new Transaction().add(ix); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); return transaction; @@ -82,7 +96,13 @@ export async function createWrappedOnAlgorand( senderAddr: string, attestVAA: Uint8Array ): Promise { - return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, attestVAA, senderAddr); + return await _submitVAAAlgorand( + client, + tokenBridgeId, + bridgeId, + attestVAA, + senderAddr + ); } export async function createWrappedOnNear( diff --git a/sdk/js/src/token_bridge/getForeignAsset.ts b/sdk/js/src/token_bridge/getForeignAsset.ts index 24aa469f3..1a4e3e1ad 100644 --- a/sdk/js/src/token_bridge/getForeignAsset.ts +++ b/sdk/js/src/token_bridge/getForeignAsset.ts @@ -1,13 +1,17 @@ -import { Connection, PublicKey } from "@solana/web3.js"; +import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; import { Algodv2 } from "algosdk"; import { AptosClient } from "aptos"; import { ethers } from "ethers"; import { fromUint8Array } from "js-base64"; -import { calcLogicSigAccount, decodeLocalState, hexToNativeAssetBigIntAlgorand } from "../algorand"; +import { + calcLogicSigAccount, + decodeLocalState, + hexToNativeAssetBigIntAlgorand, +} from "../algorand"; import { Bridge__factory } from "../ethers-contracts"; -import { importTokenWasm } from "../solana/wasm"; +import { deriveWrappedMintKey, getWrappedMeta } from "../solana/tokenBridge"; import { callFunctionNear, ChainId, @@ -36,7 +40,10 @@ export async function getForeignAssetEth( ): Promise { const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); try { - return await tokenBridge.wrappedAsset(coalesceChainId(originChain), originAsset); + return await tokenBridge.wrappedAsset( + coalesceChainId(originChain), + originAsset + ); } catch (e) { return null; } @@ -49,12 +56,15 @@ export async function getForeignAssetTerra( originAsset: Uint8Array ): Promise { try { - const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, { - wrapped_registry: { - chain: coalesceChainId(originChain), - address: fromUint8Array(originAsset), - }, - }); + const result: { address: string } = await client.wasm.contractQuery( + tokenBridgeAddress, + { + wrapped_registry: { + chain: coalesceChainId(originChain), + address: fromUint8Array(originAsset), + }, + } + ); return result.address; } catch (e) { return null; @@ -127,23 +137,24 @@ export async function getForeignAssetXpla( * @param tokenBridgeAddress * @param originChain * @param originAsset zero pad to 32 bytes + * @param [commitment] * @returns */ export async function getForeignAssetSolana( connection: Connection, - tokenBridgeAddress: string, + tokenBridgeAddress: PublicKeyInitData, originChain: ChainId | ChainName, - originAsset: Uint8Array + originAsset: Uint8Array, + commitment?: Commitment ): Promise { - const { wrapped_address } = await importTokenWasm(); - const wrappedAddress = wrapped_address( + const mint = deriveWrappedMintKey( tokenBridgeAddress, - originAsset, - coalesceChainId(originChain) + coalesceChainId(originChain) as number, + originAsset ); - const wrappedAddressPK = new PublicKey(wrappedAddress); - const wrappedAssetAccountInfo = await connection.getAccountInfo(wrappedAddressPK); - return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null; + return getWrappedMeta(connection, tokenBridgeAddress, mint, commitment) + .catch((_) => null) + .then((meta) => (meta === null ? null : mint.toString())); } export async function getForeignAssetAlgorand( @@ -165,7 +176,11 @@ export async function getForeignAssetAlgorand( if (!doesExist) { return null; } - let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, lsa.address()); + let asset: Uint8Array = await decodeLocalState( + client, + tokenBridgeId, + lsa.address() + ); if (asset.length > 8) { const tmp = Buffer.from(asset.slice(0, 8)); return tmp.readBigUInt64BE(0); diff --git a/sdk/js/src/token_bridge/getIsTransferCompleted.ts b/sdk/js/src/token_bridge/getIsTransferCompleted.ts index 9ec6c58f3..544a57d12 100644 --- a/sdk/js/src/token_bridge/getIsTransferCompleted.ts +++ b/sdk/js/src/token_bridge/getIsTransferCompleted.ts @@ -1,5 +1,5 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import { Algodv2, bigIntToBytes } from "algosdk"; import { AptosClient } from "aptos"; @@ -8,6 +8,7 @@ import { ethers } from "ethers"; import { fromUint8Array } from "js-base64"; import { redeemOnTerra } from "."; import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from ".."; +import { getClaim } from "../solana/wormhole"; import { BITS_PER_KEY, calcLogicSigAccount, @@ -17,7 +18,7 @@ import { import { callFunctionNear } from "../utils/near"; import { getSignedVAAHash } from "../bridge"; import { Bridge__factory } from "../ethers-contracts"; -import { importCoreWasm } from "../solana/wasm"; +import { parseVaa, SignedVaa } from "../vaa/wormhole"; import { safeBigIntToNumber } from "../utils/bigint"; import { Provider } from "near-api-js/lib/providers"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; @@ -26,10 +27,10 @@ import { State } from "../aptos/types"; export async function getIsTransferCompletedEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, - signedVAA: Uint8Array, + signedVAA: Uint8Array ): Promise { const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); - const signedVAAHash = await getSignedVAAHash(signedVAA); + const signedVAAHash = getSignedVAAHash(signedVAA); return await tokenBridge.isTransferCompleted(signedVAAHash); } @@ -40,16 +41,18 @@ export async function getIsTransferCompletedTerra( tokenBridgeAddress: string, signedVAA: Uint8Array, client: LCDClient, - gasPriceUrl: string, + gasPriceUrl: string ): Promise { const msg = await redeemOnTerra( tokenBridgeAddress, TERRA_REDEEMED_CHECK_WALLET_ADDRESS, - signedVAA, + signedVAA ); // TODO: remove gasPriceUrl and just use the client's gas prices const gasPrices = await axios.get(gasPriceUrl).then((result) => result.data); - const account = await client.auth.accountInfo(TERRA_REDEEMED_CHECK_WALLET_ADDRESS); + const account = await client.auth.accountInfo( + TERRA_REDEEMED_CHECK_WALLET_ADDRESS + ); try { await client.tx.estimateFee( [ @@ -63,7 +66,7 @@ export async function getIsTransferCompletedTerra( memo: "already redeemed calculation", feeDenoms: ["uluna"], gasPrices, - }, + } ); } catch (e: any) { // redeemed if the VAA was already executed @@ -136,23 +139,32 @@ export async function getIsTransferCompletedXpla( signedVAA: Uint8Array, client: XplaLCDClient ): Promise { - const result: { is_redeemed: boolean } = await client.wasm.contractQuery(tokenBridgeAddress, { - is_vaa_redeemed: { - vaa: fromUint8Array(signedVAA), - }, - }); + const result: { is_redeemed: boolean } = await client.wasm.contractQuery( + tokenBridgeAddress, + { + is_vaa_redeemed: { + vaa: fromUint8Array(signedVAA), + }, + } + ); return result.is_redeemed; } export async function getIsTransferCompletedSolana( - tokenBridgeAddress: string, - signedVAA: Uint8Array, + tokenBridgeAddress: PublicKeyInitData, + signedVAA: SignedVaa, connection: Connection, + commitment?: Commitment ): Promise { - const { claim_address } = await importCoreWasm(); - const claimAddress = await claim_address(tokenBridgeAddress, signedVAA); - const claimInfo = await connection.getAccountInfo(new PublicKey(claimAddress), "confirmed"); - return !!claimInfo; + const parsed = parseVaa(signedVAA); + return getClaim( + connection, + tokenBridgeAddress, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence, + commitment + ).catch((e) => false); } // Algorand @@ -169,7 +181,7 @@ async function checkBitsSet( client: Algodv2, appId: bigint, addr: string, - seq: bigint, + seq: bigint ): Promise { let retval: boolean = false; let appState: any[] = []; @@ -216,7 +228,7 @@ async function checkBitsSet( export async function getIsTransferCompletedAlgorand( client: Algodv2, appId: bigint, - signedVAA: Uint8Array, + signedVAA: Uint8Array ): Promise { const parsedVAA = _parseVAAAlgorand(signedVAA); const seq: bigint = parsedVAA.sequence; @@ -226,7 +238,7 @@ export async function getIsTransferCompletedAlgorand( client, appId, seq / BigInt(MAX_BITS), - chainRaw + em, + chainRaw + em ); if (!doesExist) { return false; @@ -239,7 +251,7 @@ export async function getIsTransferCompletedAlgorand( export async function getIsTransferCompletedNear( provider: Provider, tokenBridge: string, - signedVAA: Uint8Array, + signedVAA: Uint8Array ): Promise { const vaa = Buffer.from(signedVAA).toString("hex"); return ( @@ -259,17 +271,20 @@ export async function getIsTransferCompletedNear( export async function getIsTransferCompletedAptos( client: AptosClient, tokenBridgeAddress: string, - transferVAA: Uint8Array, + transferVAA: Uint8Array ): Promise { // get handle tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress); const state = ( - await client.getAccountResource(tokenBridgeAddress, `${tokenBridgeAddress}::state::State`) + await client.getAccountResource( + tokenBridgeAddress, + `${tokenBridgeAddress}::state::State` + ) ).data as State; const handle = state.consumed_vaas.elems.handle; // check if vaa hash is in consumed_vaas - const transferVAAHash = await getSignedVAAHash(transferVAA); + const transferVAAHash = getSignedVAAHash(transferVAA); try { // when accessing Set, key is type T and value is 0 await client.getTableItem(handle, { diff --git a/sdk/js/src/token_bridge/getIsWrappedAsset.ts b/sdk/js/src/token_bridge/getIsWrappedAsset.ts index 7c533cf25..2a70cac1d 100644 --- a/sdk/js/src/token_bridge/getIsWrappedAsset.ts +++ b/sdk/js/src/token_bridge/getIsWrappedAsset.ts @@ -1,19 +1,27 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, +} from "@solana/web3.js"; import { LCDClient } from "@terra-money/terra.js"; import { Algodv2, getApplicationAddress } from "algosdk"; import { AptosClient } from "aptos"; import { ethers } from "ethers"; import { Bridge__factory } from "../ethers-contracts"; -import { importTokenWasm } from "../solana/wasm"; import { CHAIN_ID_INJECTIVE, ensureHexPrefix, coalesceModuleAddress, tryNativeToHexString, } from "../utils"; +import { deriveWrappedMetaKey, getWrappedMeta } from "../solana/tokenBridge"; import { safeBigIntToNumber } from "../utils/bigint"; -import { getForeignAssetInjective } from "./getForeignAsset"; +import { + getForeignAssetSolana, + getForeignAssetInjective, +} from "./getForeignAsset"; /** * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset @@ -71,26 +79,25 @@ export async function getIsWrappedAssetInjective( * @param connection * @param tokenBridgeAddress * @param mintAddress + * @param [commitment] * @returns */ -export async function getIsWrappedAssetSol( +export async function getIsWrappedAssetSolana( connection: Connection, - tokenBridgeAddress: string, - mintAddress: string + tokenBridgeAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + commitment?: Commitment ): Promise { - if (!mintAddress) return false; - const { wrapped_meta_address } = await importTokenWasm(); - const wrappedMetaAddress = wrapped_meta_address( - tokenBridgeAddress, - new PublicKey(mintAddress).toBytes() - ); - const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress); - const wrappedMetaAccountInfo = await connection.getAccountInfo( - wrappedMetaAddressPK - ); - return !!wrappedMetaAccountInfo; + if (!mintAddress) { + return false; + } + return getWrappedMeta(connection, tokenBridgeAddress, mintAddress, commitment) + .catch((_) => null) + .then((meta) => meta != null); } +export const getIsWrappedAssetSol = getIsWrappedAssetSolana; + /** * Returns whethor or not an asset on Algorand is a wormhole wrapped asset * @param client Algodv2 client diff --git a/sdk/js/src/token_bridge/getOriginalAsset.ts b/sdk/js/src/token_bridge/getOriginalAsset.ts index 3b2acb3fa..e6a7274fc 100644 --- a/sdk/js/src/token_bridge/getOriginalAsset.ts +++ b/sdk/js/src/token_bridge/getOriginalAsset.ts @@ -1,5 +1,10 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { + Commitment, + Connection, + PublicKey, + PublicKeyInitData, +} from "@solana/web3.js"; import { LCDClient as TerraLCDClient } from "@terra-money/terra.js"; import { Algodv2 } from "algosdk"; import { ethers } from "ethers"; @@ -7,7 +12,6 @@ import { arrayify, sha256, zeroPad } from "ethers/lib/utils"; import { decodeLocalState } from "../algorand"; import { buildTokenId, isNativeCosmWasmDenom } from "../cosmwasm/address"; import { TokenImplementation__factory } from "../ethers-contracts"; -import { importTokenWasm } from "../solana/wasm"; import { buildNativeId } from "../terra"; import { canonicalAddress } from "../cosmos"; import { @@ -38,8 +42,9 @@ import { import { Provider } from "near-api-js/lib/providers"; import { LCDClient as XplaLCDClient } from "@xpla/xpla.js"; import { AptosClient } from "aptos"; -import { OriginInfo } from "../aptos/types" -import { sha3_256 } from "js-sha3";; +import { OriginInfo } from "../aptos/types"; +import { sha3_256 } from "js-sha3"; +import { getWrappedMeta } from "../solana/tokenBridge"; // TODO: remove `as ChainId` and return number in next minor version as we can't ensure it will match our type definition export interface WormholeWrappedInfo { @@ -203,48 +208,51 @@ export async function getOriginalAssetCosmWasm( * @param connection * @param tokenBridgeAddress * @param mintAddress + * @param [commitment] * @returns */ -export async function getOriginalAssetSol( +export async function getOriginalAssetSolana( connection: Connection, - tokenBridgeAddress: string, - mintAddress: string + tokenBridgeAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + commitment?: Commitment ): Promise { - if (mintAddress) { - // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something - const { parse_wrapped_meta, wrapped_meta_address } = - await importTokenWasm(); - const wrappedMetaAddress = wrapped_meta_address( - tokenBridgeAddress, - new PublicKey(mintAddress).toBytes() - ); - const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress); - const wrappedMetaAccountInfo = await connection.getAccountInfo( - wrappedMetaAddressPK - ); - if (wrappedMetaAccountInfo) { - const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data); - return { - isWrapped: true, - chainId: parsed.chain, - assetAddress: parsed.token_address, - }; - } - } try { + const mint = new PublicKey(mintAddress); + + return getWrappedMeta( + connection, + tokenBridgeAddress, + mintAddress, + commitment + ) + .catch((_) => null) + .then((meta) => { + if (meta === null) { + return { + isWrapped: false, + chainId: CHAIN_ID_SOLANA, + assetAddress: mint.toBytes(), + }; + } else { + return { + isWrapped: true, + chainId: meta.chain as ChainId, + assetAddress: Uint8Array.from(meta.tokenAddress), + }; + } + }); + } catch (_) { return { isWrapped: false, chainId: CHAIN_ID_SOLANA, - assetAddress: new PublicKey(mintAddress).toBytes(), + assetAddress: new Uint8Array(32), }; - } catch (e) {} - return { - isWrapped: false, - chainId: CHAIN_ID_SOLANA, - assetAddress: new Uint8Array(32), - }; + } } +export const getOriginalAssetSol = getOriginalAssetSolana; + /** * Returns an origin chain and asset address on {originChain} for a provided Wormhole wrapped address * @param client Algodv2 client diff --git a/sdk/js/src/token_bridge/redeem.ts b/sdk/js/src/token_bridge/redeem.ts index 849056997..f42a7583c 100644 --- a/sdk/js/src/token_bridge/redeem.ts +++ b/sdk/js/src/token_bridge/redeem.ts @@ -1,8 +1,19 @@ -import { AccountLayout, Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; import { + AccountLayout, + createCloseAccountInstruction, + createInitializeAccountInstruction, + createTransferInstruction, + getMinimumBalanceForRentExemptMint, + getMint, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + Commitment, Connection, Keypair, PublicKey, + PublicKeyInitData, SystemProgram, Transaction, } from "@solana/web3.js"; @@ -10,30 +21,33 @@ import { MsgExecuteContract } from "@terra-money/terra.js"; import { Algodv2 } from "algosdk"; import { ethers, Overrides } from "ethers"; import { fromUint8Array } from "js-base64"; -import { TransactionSignerPair, _submitVAAAlgorand } from "../algorand"; +import { + Account as nearAccount, + providers as nearProviders, +} from "near-api-js"; +import BN from "bn.js"; +import { + TransactionSignerPair, + _parseVAAAlgorand, + _submitVAAAlgorand, +} from "../algorand"; import { Bridge__factory } from "../ethers-contracts"; -import { ixFromRust } from "../solana"; -import { importCoreWasm, importTokenWasm } from "../solana/wasm"; import { CHAIN_ID_NEAR, CHAIN_ID_SOLANA, ChainId, MAX_VAA_DECIMALS, - WSOL_ADDRESS, - WSOL_DECIMALS, uint8ArrayToHex, callFunctionNear, hashLookup } from "../utils"; - -import { getForeignAssetNear } from "."; - -import { _parseVAAAlgorand } from "../algorand"; - -import { hexToNativeString } from "../utils/array"; -import { parseTransferPayload } from "../utils/parseVaa"; -import BN from "bn.js"; import { MsgExecuteContract as MsgExecuteContractInjective } from "@injectivelabs/sdk-ts"; +import { + createCompleteTransferNativeInstruction, + createCompleteTransferWrappedInstruction, +} from "../solana/tokenBridge"; +import { SignedVaa, parseTokenTransferVaa } from "../vaa"; +import { getForeignAssetNear } from "./getForeignAsset"; import { FunctionCallOptions } from "near-api-js/lib/account"; import { Provider } from "near-api-js/lib/providers"; import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js"; @@ -113,41 +127,30 @@ export function redeemOnXpla( export async function redeemAndUnwrapOnSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - signedVAA: Uint8Array + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + signedVaa: SignedVaa, + commitment?: Commitment ) { - const { parse_vaa } = await importCoreWasm(); - const { complete_transfer_native_ix } = await importTokenWasm(); - const parsedVAA = parse_vaa(signedVAA); - const parsedPayload = parseTransferPayload( - Buffer.from(new Uint8Array(parsedVAA.payload)) + const parsed = parseTokenTransferVaa(signedVaa); + const targetPublicKey = new PublicKey(parsed.to); + const targetAmount = await getMint(connection, NATIVE_MINT, commitment).then( + (info) => + parsed.amount * BigInt(Math.pow(10, info.decimals - MAX_VAA_DECIMALS)) ); - const targetAddress = hexToNativeString( - parsedPayload.targetAddress, - CHAIN_ID_SOLANA - ); - if (!targetAddress) { - throw new Error("Failed to read the target address."); + const rentBalance = await getMinimumBalanceForRentExemptMint(connection); + if (Buffer.compare(parsed.tokenAddress, NATIVE_MINT.toBuffer()) != 0) { + return Promise.reject("tokenAddress != NATIVE_MINT"); } - const targetPublicKey = new PublicKey(targetAddress); - const targetAmount = - parsedPayload.amount * - BigInt(WSOL_DECIMALS - MAX_VAA_DECIMALS) * - BigInt(10); - const rentBalance = await Token.getMinBalanceRentForExemptAccount(connection); - const mintPublicKey = new PublicKey(WSOL_ADDRESS); const payerPublicKey = new PublicKey(payerAddress); const ancillaryKeypair = Keypair.generate(); - const completeTransferIx = ixFromRust( - complete_transfer_native_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - signedVAA - ) + const completeTransferIx = createCompleteTransferNativeInstruction( + tokenBridgeAddress, + bridgeAddress, + payerPublicKey, + signedVaa ); //This will create a temporary account where the wSOL will be moved @@ -160,88 +163,66 @@ export async function redeemAndUnwrapOnSolana( }); //Initialize the account as a WSOL account, with the original payerAddress as owner - const initAccountIx = await Token.createInitAccountInstruction( - TOKEN_PROGRAM_ID, - mintPublicKey, + const initAccountIx = await createInitializeAccountInstruction( ancillaryKeypair.publicKey, + NATIVE_MINT, payerPublicKey ); //Send in the amount of wSOL which we want converted to SOL - const balanceTransferIx = Token.createTransferInstruction( - TOKEN_PROGRAM_ID, + const balanceTransferIx = createTransferInstruction( targetPublicKey, ancillaryKeypair.publicKey, payerPublicKey, - [], - new u64(targetAmount.toString(16), 16) + targetAmount.valueOf() ); //Close the ancillary account for cleanup. Payer address receives any remaining funds - const closeAccountIx = Token.createCloseAccountInstruction( - TOKEN_PROGRAM_ID, + const closeAccountIx = createCloseAccountInstruction( ancillaryKeypair.publicKey, //account to close payerPublicKey, //Remaining funds destination - payerPublicKey, //authority - [] + payerPublicKey //authority ); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); const transaction = new Transaction(); transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - transaction.add(completeTransferIx); - transaction.add(createAncillaryAccountIx); - transaction.add(initAccountIx); - transaction.add(balanceTransferIx); - transaction.add(closeAccountIx); + transaction.feePayer = payerPublicKey; + transaction.add( + completeTransferIx, + createAncillaryAccountIx, + initAccountIx, + balanceTransferIx, + closeAccountIx + ); transaction.partialSign(ancillaryKeypair); return transaction; } export async function redeemOnSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - signedVAA: Uint8Array, - feeRecipientAddress?: string + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + signedVaa: SignedVaa, + feeRecipientAddress?: PublicKeyInitData, + commitment?: Commitment ) { - const { parse_vaa } = await importCoreWasm(); - const parsedVAA = parse_vaa(signedVAA); - const isSolanaNative = - Buffer.from(new Uint8Array(parsedVAA.payload)).readUInt16BE(65) === - CHAIN_ID_SOLANA; - const { complete_transfer_wrapped_ix, complete_transfer_native_ix } = - await importTokenWasm(); - const ixs = []; - if (isSolanaNative) { - ixs.push( - ixFromRust( - complete_transfer_native_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - signedVAA, - feeRecipientAddress - ) - ) - ); - } else { - ixs.push( - ixFromRust( - complete_transfer_wrapped_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - signedVAA, - feeRecipientAddress - ) - ) - ); - } - const transaction = new Transaction().add(...ixs); - const { blockhash } = await connection.getRecentBlockhash(); + const parsed = parseTokenTransferVaa(signedVaa); + const createCompleteTransferInstruction = + parsed.tokenChain == CHAIN_ID_SOLANA + ? createCompleteTransferNativeInstruction + : createCompleteTransferWrappedInstruction; + const transaction = new Transaction().add( + createCompleteTransferInstruction( + tokenBridgeAddress, + bridgeAddress, + payerAddress, + parsed, + feeRecipientAddress + ) + ); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); return transaction; diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index b6ce1597c..3a2c6cae9 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -1,8 +1,17 @@ -import { AccountLayout, Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; import { + AccountLayout, + createCloseAccountInstruction, + createInitializeAccountInstruction, + getMinimumBalanceForRentExemptMint, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + Commitment, Connection, Keypair, PublicKey, + PublicKeyInitData, SystemProgram, Transaction as SolanaTransaction, } from "@solana/web3.js"; @@ -20,7 +29,13 @@ import { Transaction as AlgorandTransaction, } from "algosdk"; import { ethers, Overrides, PayableOverrides } from "ethers"; -import { getIsWrappedAssetNear, isNativeDenom } from ".."; +import BN from "bn.js"; +import { + Account as nearAccount, + providers as nearProviders, +} from "near-api-js"; +import { isNativeDenom } from "../terra"; +import { getIsWrappedAssetNear } from ".."; import { assetOptinCheck, getMessageFee, @@ -32,24 +47,28 @@ import { Bridge__factory, TokenImplementation__factory, } from "../ethers-contracts"; -import { getBridgeFeeIx, ixFromRust } from "../solana"; -import { importTokenWasm } from "../solana/wasm"; +import { createBridgeFeeTransferInstruction } from "../solana"; +import { + createApproveAuthoritySignerInstruction, + createTransferNativeInstruction, + createTransferNativeWithPayloadInstruction, + createTransferWrappedInstruction, + createTransferWrappedWithPayloadInstruction, +} from "../solana/tokenBridge"; import { ChainId, ChainName, - WSOL_ADDRESS, coalesceChainId, createNonce, hexToUint8Array, + safeBigIntToNumber, textToUint8Array, uint8ArrayToHex, CHAIN_ID_SOLANA, callFunctionNear, } from "../utils"; -import { safeBigIntToNumber } from "../utils/bigint"; import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm"; import { Types } from "aptos"; -const BN = require("bn.js"); import { FunctionCallOptions } from "near-api-js/lib/account"; import { Provider } from "near-api-js/lib/providers"; import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js"; @@ -424,18 +443,18 @@ export function transferFromXpla( export async function transferNativeSol( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - amount: BigInt, - targetAddress: Uint8Array, + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + amount: bigint, + targetAddress: Uint8Array | Buffer, targetChain: ChainId | ChainName, - relayerFee: BigInt = BigInt(0), - payload: Uint8Array | null = null + relayerFee: bigint = BigInt(0), + payload: Uint8Array | Buffer | null = null, + commitment?: Commitment ) { - //https://github.com/solana-labs/solana-program-library/blob/master/token/js/client/token.js - const rentBalance = await Token.getMinBalanceRentForExemptAccount(connection); - const mintPublicKey = new PublicKey(WSOL_ADDRESS); + const rentBalance = await getMinimumBalanceForRentExemptMint(connection); + const mintPublicKey = NATIVE_MINT; const payerPublicKey = new PublicKey(payerAddress); const ancillaryKeypair = Keypair.generate(); @@ -455,200 +474,191 @@ export async function transferNativeSol( toPubkey: ancillaryKeypair.publicKey, }); //Initialize the account as a WSOL account, with the original payerAddress as owner - const initAccountIx = await Token.createInitAccountInstruction( - TOKEN_PROGRAM_ID, - mintPublicKey, + const initAccountIx = await createInitializeAccountInstruction( ancillaryKeypair.publicKey, + mintPublicKey, payerPublicKey ); //Normal approve & transfer instructions, except that the wSOL is sent from the ancillary account. - const { - transfer_native_ix, - transfer_native_with_payload_ix, - approval_authority_address, - } = await importTokenWasm(); const nonce = createNonce().readUInt32LE(0); - const transferIx = await getBridgeFeeIx( + const transferIx = await createBridgeFeeTransferInstruction( connection, bridgeAddress, payerAddress ); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, + const approvalIx = createApproveAuthoritySignerInstruction( + tokenBridgeAddress, ancillaryKeypair.publicKey, - new PublicKey(approval_authority_address(tokenBridgeAddress)), - payerPublicKey, //owner - [], - new u64(amount.toString(16), 16) + payerPublicKey, + amount ); - let messageKey = Keypair.generate(); - const ix = ixFromRust( + const message = Keypair.generate(); + const tokenBridgeTransferIx = payload !== null - ? transfer_native_with_payload_ix( + ? createTransferNativeWithPayloadInstruction( tokenBridgeAddress, bridgeAddress, payerAddress, - messageKey.publicKey.toString(), - ancillaryKeypair.publicKey.toString(), - WSOL_ADDRESS, + message.publicKey, + ancillaryKeypair.publicKey, + NATIVE_MINT, nonce, - amount.valueOf(), - targetAddress, + amount, + Buffer.from(targetAddress), coalesceChainId(targetChain), payload ) - : transfer_native_ix( + : createTransferNativeInstruction( tokenBridgeAddress, bridgeAddress, payerAddress, - messageKey.publicKey.toString(), - ancillaryKeypair.publicKey.toString(), - WSOL_ADDRESS, + message.publicKey, + ancillaryKeypair.publicKey, + NATIVE_MINT, nonce, - amount.valueOf(), - relayerFee.valueOf(), - targetAddress, + amount, + relayerFee, + Buffer.from(targetAddress), coalesceChainId(targetChain) - ) - ); + ); //Close the ancillary account for cleanup. Payer address receives any remaining funds - const closeAccountIx = Token.createCloseAccountInstruction( - TOKEN_PROGRAM_ID, + const closeAccountIx = createCloseAccountInstruction( ancillaryKeypair.publicKey, //account to close payerPublicKey, //Remaining funds destination - payerPublicKey, //authority - [] + payerPublicKey //authority ); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); const transaction = new SolanaTransaction(); transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payerAddress); - transaction.add(createAncillaryAccountIx); - transaction.add(initialBalanceTransferIx); - transaction.add(initAccountIx); - transaction.add(transferIx, approvalIx, ix); - transaction.add(closeAccountIx); - transaction.partialSign(messageKey); - transaction.partialSign(ancillaryKeypair); + transaction.feePayer = payerPublicKey; + transaction.add( + createAncillaryAccountIx, + initialBalanceTransferIx, + initAccountIx, + transferIx, + approvalIx, + tokenBridgeTransferIx, + closeAccountIx + ); + transaction.partialSign(message, ancillaryKeypair); return transaction; } export async function transferFromSolana( connection: Connection, - bridgeAddress: string, - tokenBridgeAddress: string, - payerAddress: string, - fromAddress: string, - mintAddress: string, - amount: BigInt, - targetAddress: Uint8Array, + bridgeAddress: PublicKeyInitData, + tokenBridgeAddress: PublicKeyInitData, + payerAddress: PublicKeyInitData, + fromAddress: PublicKeyInitData, + mintAddress: PublicKeyInitData, + amount: bigint, + targetAddress: Uint8Array | Buffer, targetChain: ChainId | ChainName, - originAddress?: Uint8Array, + originAddress?: Uint8Array | Buffer, originChain?: ChainId | ChainName, - fromOwnerAddress?: string, - relayerFee: BigInt = BigInt(0), - payload: Uint8Array | null = null + fromOwnerAddress?: PublicKeyInitData, + relayerFee: bigint = BigInt(0), + payload: Uint8Array | Buffer | null = null, + commitment?: Commitment ) { const originChainId: ChainId | undefined = originChain ? coalesceChainId(originChain) : undefined; + if (fromOwnerAddress === undefined) { + fromOwnerAddress = payerAddress; + } const nonce = createNonce().readUInt32LE(0); - const transferIx = await getBridgeFeeIx( + const transferIx = await createBridgeFeeTransferInstruction( connection, bridgeAddress, payerAddress ); - const { - transfer_native_ix, - transfer_wrapped_ix, - transfer_native_with_payload_ix, - transfer_wrapped_with_payload_ix, - approval_authority_address, - } = await importTokenWasm(); - const approvalIx = Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - new PublicKey(fromAddress), - new PublicKey(approval_authority_address(tokenBridgeAddress)), - new PublicKey(fromOwnerAddress || payerAddress), - [], - new u64(amount.toString(16), 16) + const approvalIx = createApproveAuthoritySignerInstruction( + tokenBridgeAddress, + fromAddress, + fromOwnerAddress, + amount ); - let messageKey = Keypair.generate(); + const message = Keypair.generate(); const isSolanaNative = originChainId === undefined || originChainId === CHAIN_ID_SOLANA; if (!isSolanaNative && !originAddress) { - throw new Error("originAddress is required when specifying originChain"); + return Promise.reject( + "originAddress is required when specifying originChain" + ); } - const ix = ixFromRust( - isSolanaNative - ? payload !== null - ? transfer_native_with_payload_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - messageKey.publicKey.toString(), - fromAddress, - mintAddress, - nonce, - amount.valueOf(), - targetAddress, - coalesceChainId(targetChain), - payload - ) - : transfer_native_ix( - tokenBridgeAddress, - bridgeAddress, - payerAddress, - messageKey.publicKey.toString(), - fromAddress, - mintAddress, - nonce, - amount.valueOf(), - relayerFee.valueOf(), - targetAddress, - coalesceChainId(targetChain) - ) - : payload !== null - ? transfer_wrapped_with_payload_ix( + const tokenBridgeTransferIx = isSolanaNative + ? payload !== null + ? createTransferNativeWithPayloadInstruction( tokenBridgeAddress, bridgeAddress, payerAddress, - messageKey.publicKey.toString(), + message.publicKey, fromAddress, - fromOwnerAddress || payerAddress, - originChainId as number, // checked by isSolanaNative - originAddress as Uint8Array, // checked by throw + mintAddress, nonce, - amount.valueOf(), + amount, targetAddress, coalesceChainId(targetChain), payload ) - : transfer_wrapped_ix( + : createTransferNativeInstruction( tokenBridgeAddress, bridgeAddress, payerAddress, - messageKey.publicKey.toString(), + message.publicKey, fromAddress, - fromOwnerAddress || payerAddress, - originChainId as number, // checked by isSolanaNative - originAddress as Uint8Array, // checked by throw + mintAddress, nonce, - amount.valueOf(), - relayerFee.valueOf(), + amount, + relayerFee, targetAddress, coalesceChainId(targetChain) ) + : payload !== null + ? createTransferWrappedWithPayloadInstruction( + tokenBridgeAddress, + bridgeAddress, + payerAddress, + message.publicKey, + fromAddress, + fromOwnerAddress, + originChainId!, + originAddress!, + nonce, + amount, + targetAddress, + coalesceChainId(targetChain), + payload + ) + : createTransferWrappedInstruction( + tokenBridgeAddress, + bridgeAddress, + payerAddress, + message.publicKey, + fromAddress, + fromOwnerAddress, + originChainId!, + originAddress!, + nonce, + amount, + relayerFee, + targetAddress, + coalesceChainId(targetChain) + ); + const transaction = new SolanaTransaction().add( + transferIx, + approvalIx, + tokenBridgeTransferIx ); - const transaction = new SolanaTransaction().add(transferIx, approvalIx, ix); - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(payerAddress); - transaction.partialSign(messageKey); + transaction.partialSign(message); return transaction; } diff --git a/sdk/js/src/utils/index.ts b/sdk/js/src/utils/index.ts index 694d26819..eaf7fd403 100644 --- a/sdk/js/src/utils/index.ts +++ b/sdk/js/src/utils/index.ts @@ -5,3 +5,4 @@ export * from "./consts"; export * from "./createNonce"; export * from "./near"; export * from "./parseVaa"; +export * from "./keccak"; diff --git a/sdk/js/src/utils/keccak.ts b/sdk/js/src/utils/keccak.ts new file mode 100644 index 000000000..88f14f709 --- /dev/null +++ b/sdk/js/src/utils/keccak.ts @@ -0,0 +1,5 @@ +import { ethers } from "ethers"; + +export function keccak256(data: ethers.BytesLike): Buffer { + return Buffer.from(ethers.utils.arrayify(ethers.utils.keccak256(data))); +} diff --git a/sdk/js/src/utils/parseVaa.ts b/sdk/js/src/utils/parseVaa.ts index a172a15f4..a09b3200d 100644 --- a/sdk/js/src/utils/parseVaa.ts +++ b/sdk/js/src/utils/parseVaa.ts @@ -1,10 +1,9 @@ import { BigNumber } from "@ethersproject/bignumber"; +import { parseNftTransferPayload, parseTokenTransferPayload } from "../vaa"; import { ChainId } from "./consts"; export const METADATA_REPLACE = new RegExp("\u0000", "g"); -// TODO: remove `as ChainId` in next minor version as we can't ensure it will match our type definition - // note: actual first byte is message type // 0 [u8; 32] token_address // 32 u16 token_chain @@ -15,36 +14,19 @@ export const METADATA_REPLACE = new RegExp("\u0000", "g"); // 131 [u8;len] uri // ? [u8; 32] recipient // ? u16 recipient_chain -export const parseNFTPayload = (arr: Buffer) => { - const originAddress = arr.slice(1, 1 + 32).toString("hex"); - const originChain = arr.readUInt16BE(33) as ChainId; - const symbol = Buffer.from(arr.slice(35, 35 + 32)) - .toString("utf8") - .replace(METADATA_REPLACE, ""); - const name = Buffer.from(arr.slice(67, 67 + 32)) - .toString("utf8") - .replace(METADATA_REPLACE, ""); - const tokenId = BigNumber.from(arr.slice(99, 99 + 32)); - const uri_len = arr.readUInt8(131); - const uri = Buffer.from(arr.slice(132, 132 + uri_len)) - .toString("utf8") - .replace(METADATA_REPLACE, ""); - const target_offset = 132 + uri_len; - const targetAddress = arr - .slice(target_offset, target_offset + 32) - .toString("hex"); - const targetChain = arr.readUInt16BE(target_offset + 32) as ChainId; +export function parseNFTPayload(payload: Buffer) { + const parsed = parseNftTransferPayload(payload); return { - originAddress, - originChain, - symbol, - name, - tokenId, - uri, - targetAddress, - targetChain, + originAddress: parsed.tokenAddress.toString("hex"), + originChain: parsed.tokenChain, + symbol: parsed.symbol, + name: parsed.name, + tokenId: BigNumber.from(parsed.tokenId), + uri: parsed.uri, + targetAddress: parsed.to.toString("hex"), + targetChain: parsed.toChain, }; -}; +} // 0 u256 amount // 32 [u8; 32] token_address @@ -52,14 +34,21 @@ export const parseNFTPayload = (arr: Buffer) => { // 66 [u8; 32] recipient // 98 u16 recipient_chain // 100 u256 fee -export const parseTransferPayload = (arr: Buffer) => ({ - amount: BigNumber.from(arr.slice(1, 1 + 32)).toBigInt(), - originAddress: arr.slice(33, 33 + 32).toString("hex"), - originChain: arr.readUInt16BE(65) as ChainId, - targetAddress: arr.slice(67, 67 + 32).toString("hex"), - targetChain: arr.readUInt16BE(99) as ChainId, - fee: BigNumber.from(arr.slice(101, 101 + 32)).toBigInt(), -}); +export function parseTransferPayload(payload: Buffer) { + const parsed = parseTokenTransferPayload(payload); + return { + amount: parsed.amount, + originAddress: parsed.tokenAddress.toString("hex"), + originChain: parsed.tokenChain, + targetAddress: parsed.to.toString("hex"), + targetChain: parsed.toChain, + fee: parsed.fee === null ? undefined : parsed.fee, + fromAddress: + parsed.fromAddress === null + ? undefined + : parsed.fromAddress.toString("hex"), + }; +} //This returns a corrected amount, which accounts for the difference between the VAA //decimals, and the decimals of the asset. diff --git a/sdk/js/src/utils/solana.ts b/sdk/js/src/utils/solana.ts deleted file mode 100644 index 53b041a9b..000000000 --- a/sdk/js/src/utils/solana.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Connection, PublicKey, Transaction } from "@solana/web3.js"; - -/* - The transactions provided to this function should be ready to be sent. - This function will only add the feePayer and blockhash, and then sign, send, and confirm the transaction. -*/ -export async function sendAndConfirmTransactionsWithRetry( - connection: Connection, - signTransaction: (transaction: Transaction) => Promise, - payer: string, - unsignedTransactions: Transaction[], - maxRetries: number = 0 -) { - if (!(unsignedTransactions && unsignedTransactions.length)) { - return Promise.reject("No transactions provided to send."); - } - let currentRetries = 0; - let currentIndex = 0; - const transactionReceipts = []; - while ( - !(currentIndex >= unsignedTransactions.length) && - !(currentRetries > maxRetries) - ) { - let transaction = unsignedTransactions[currentIndex]; - let signed = null; - try { - const { blockhash } = await connection.getRecentBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = new PublicKey(payer); - } catch (e) { - console.error(e); - currentRetries++; - //Behavior after this is undefined, so best just to restart and try again. - continue; - } - try { - signed = await signTransaction(transaction); - } catch (e) { - //Eject here because this is most likely an intentional rejection from the user, or a genuine unrecoverable failure. - return Promise.reject("Failed to sign transaction."); - } - try { - const txid = await connection.sendRawTransaction(signed.serialize()); - const receipt = await connection.confirmTransaction(txid); - transactionReceipts.push(receipt); - currentIndex++; - } catch (e) { - console.error(e); - currentRetries++; - } - } - - if (currentRetries > maxRetries) { - return Promise.reject("Reached the maximum number of retries."); - } else { - return Promise.resolve(transactionReceipts); - } -} diff --git a/sdk/js/src/vaa/generic.ts b/sdk/js/src/vaa/generic.ts new file mode 100644 index 000000000..0430fe60c --- /dev/null +++ b/sdk/js/src/vaa/generic.ts @@ -0,0 +1,872 @@ +import { Parser } from "binary-parser"; +import { BigNumber, ethers } from "ethers"; +import { solidityKeccak256 } from "ethers/lib/utils"; +import * as elliptic from "elliptic"; + +export interface Signature { + guardianSetIndex: number; + signature: string; +} + +export interface VAA { + version: number; + guardianSetIndex: number; + signatures: Signature[]; + timestamp: number; + nonce: number; + emitterChain: number; + emitterAddress: string; + sequence: bigint; + consistencyLevel: number; + payload: T; +} + +class P { + private parser: Parser; + constructor(parser: Parser) { + this.parser = parser; + } + + // Try to parse a buffer with a parser, and return null if it failed due to an + // assertion error. + parse(buffer: Buffer): T | null { + try { + let result = this.parser.parse(buffer); + delete result["end"]; + return result; + } catch (e: any) { + if (e.message?.includes("Assertion error")) { + return null; + } else { + throw e; + } + } + } + + or(other: P): P { + let p = new P(other.parser); + p.parse = (buffer: Buffer): T | U | null => { + return this.parse(buffer) ?? other.parse(buffer); + }; + return p; + } +} + +export interface Other { + type: "Other"; + hex: string; + ascii?: string; +} + +// All the different types of payloads +export type Payload = + | GuardianSetUpgrade + | CoreContractUpgrade + | PortalContractUpgrade<"TokenBridge"> + | PortalContractUpgrade<"NFTBridge"> + | PortalRegisterChain<"TokenBridge"> + | PortalRegisterChain<"NFTBridge"> + | TokenBridgeTransfer + | TokenBridgeTransferWithPayload + | TokenBridgeAttestMeta + | NFTBridgeTransfer; + +export type ContractUpgrade = + | CoreContractUpgrade + | PortalContractUpgrade<"TokenBridge"> + | PortalContractUpgrade<"NFTBridge">; + +export function parse(buffer: Buffer): VAA { + const vaa = parseEnvelope(buffer); + const parser = guardianSetUpgradeParser + .or(coreContractUpgradeParser) + .or(portalContractUpgradeParser("TokenBridge")) + .or(portalContractUpgradeParser("NFTBridge")) + .or(portalRegisterChainParser("TokenBridge")) + .or(portalRegisterChainParser("NFTBridge")) + .or(tokenBridgeTransferParser()) + .or(tokenBridgeTransferWithPayloadParser()) + .or(tokenBridgeAttestMetaParser()) + .or(nftBridgeTransferParser()); + let payload: Payload | Other | null = parser.parse(vaa.payload); + if (payload === null) { + payload = { + type: "Other", + hex: Buffer.from(vaa.payload).toString("hex"), + ascii: Buffer.from(vaa.payload).toString("utf8"), + }; + } else { + // @ts-ignore + delete payload["tokenURILength"]; + } + var myVAA = { ...vaa, payload }; + + return myVAA; +} + +export function assertKnownPayload( + vaa: VAA +): asserts vaa is VAA { + if (vaa.payload.type === "Other") { + throw Error(`Couldn't parse VAA payload: ${vaa.payload.hex}`); + } +} + +// Parse the VAA envelope without looking into the payload. +// If you want to parse the payload as well, use 'parse'. +export function parseEnvelope(buffer: Buffer): VAA { + var vaa = vaaParser.parse(buffer); + delete vaa["end"]; + delete vaa["signatureCount"]; + vaa.payload = Buffer.from(vaa.payload); + return vaa; +} + +// Parse a signature +const signatureParser = new Parser() + .endianess("big") + .uint8("guardianSetIndex") + .array("signature", { + type: "uint8", + lengthInBytes: 65, + formatter: (arr) => Buffer.from(arr).toString("hex"), + }); + +function serialiseSignature(sig: Signature): string { + const body = [encode("uint8", sig.guardianSetIndex), sig.signature]; + return body.join(""); +} + +// Parse a vaa envelope. The payload is returned as a byte array. +const vaaParser = new Parser() + .endianess("big") + .uint8("version") + .uint32("guardianSetIndex") + .uint8("signatureCount") + .array("signatures", { + type: signatureParser, + length: "signatureCount", + }) + .uint32("timestamp") + .uint32("nonce") + .uint16("emitterChain") + .array("emitterAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint64("sequence") + .uint8("consistencyLevel") + .array("payload", { + type: "uint8", + readUntil: "eof", + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }); + +export function serialiseVAA(vaa: VAA) { + const body = [ + encode("uint8", vaa.version), + encode("uint32", vaa.guardianSetIndex), + encode("uint8", vaa.signatures.length), + ...vaa.signatures.map((sig) => serialiseSignature(sig)), + vaaBody(vaa), + ]; + return body.join(""); +} + +export function vaaDigest(vaa: VAA) { + return solidityKeccak256( + ["bytes"], + [solidityKeccak256(["bytes"], ["0x" + vaaBody(vaa)])] + ); +} + +function vaaBody(vaa: VAA) { + let payload_str: string; + if (vaa.payload.type === "Other") { + payload_str = vaa.payload.hex; + } else { + let payload = vaa.payload; + switch (payload.module) { + case "Core": + switch (payload.type) { + case "GuardianSetUpgrade": + payload_str = serialiseGuardianSetUpgrade(payload); + break; + case "ContractUpgrade": + payload_str = serialiseCoreContractUpgrade(payload); + break; + default: + return impossible(payload); + } + break; + case "NFTBridge": + switch (payload.type) { + case "ContractUpgrade": + payload_str = serialisePortalContractUpgrade(payload); + break; + case "RegisterChain": + payload_str = serialisePortalRegisterChain(payload); + break; + case "Transfer": + payload_str = serialiseNFTBridgeTransfer(payload); + break; + default: + return impossible(payload); + } + break; + case "TokenBridge": + switch (payload.type) { + case "ContractUpgrade": + payload_str = serialisePortalContractUpgrade(payload); + break; + case "RegisterChain": + payload_str = serialisePortalRegisterChain(payload); + break; + case "Transfer": + payload_str = serialiseTokenBridgeTransfer(payload); + break; + case "TransferWithPayload": + payload_str = serialiseTokenBridgeTransferWithPayload(payload); + break; + case "AttestMeta": + payload_str = serialiseTokenBridgeAttestMeta(payload); + break; + default: + return impossible(payload); + } + break; + default: + return impossible(payload); + } + } + const body = [ + encode("uint32", vaa.timestamp), + encode("uint32", vaa.nonce), + encode("uint16", vaa.emitterChain), + encode("bytes32", hex(vaa.emitterAddress)), + encode("uint64", vaa.sequence), + encode("uint8", vaa.consistencyLevel), + payload_str, + ]; + return body.join(""); +} + +export function sign(signers: string[], vaa: VAA): Signature[] { + const hash = vaaDigest(vaa); + const ec = new elliptic.ec("secp256k1"); + + return signers.map((signer, i) => { + const key = ec.keyFromPrivate(signer); + const signature = key.sign(Buffer.from(hash.substr(2), "hex"), { + canonical: true, + }); + const packed = [ + signature.r.toString("hex").padStart(64, "0"), + signature.s.toString("hex").padStart(64, "0"), + encode("uint8", signature.recoveryParam), + ].join(""); + return { + guardianSetIndex: i, + signature: packed, + }; + }); +} + +// Parse an address of given length, and render it as hex +const addressParser = (length: number) => + new Parser().endianess("big").array("address", { + type: "uint8", + lengthInBytes: length, + formatter: (arr) => Buffer.from(arr).toString("hex"), + }); + +//////////////////////////////////////////////////////////////////////////////// +// Guardian set upgrade + +export interface GuardianSetUpgrade { + module: "Core"; + type: "GuardianSetUpgrade"; + chain: number; + newGuardianSetIndex: number; + newGuardianSetLength: number; + newGuardianSet: string[]; +} + +// Parse a guardian set upgrade payload +const guardianSetUpgradeParser: P = new P( + new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from("Core").toString("hex").padStart(64, "0"), + formatter: (_str) => "Core", + }) + .uint8("type", { + assert: 2, + formatter: (_action) => "GuardianSetUpgrade", + }) + .uint16("chain") + .uint32("newGuardianSetIndex") + .uint8("newGuardianSetLength") + .array("newGuardianSet", { + type: addressParser(20), + length: "newGuardianSetLength", + formatter: (arr: [{ address: string }]) => + arr.map((addr) => addr.address), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) +); + +function serialiseGuardianSetUpgrade(payload: GuardianSetUpgrade): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 2), + encode("uint16", payload.chain), + encode("uint32", payload.newGuardianSetIndex), + encode("uint8", payload.newGuardianSet.length), + ...payload.newGuardianSet, + ]; + return body.join(""); +} + +//////////////////////////////////////////////////////////////////////////////// +// Contract upgrades + +export interface CoreContractUpgrade { + module: "Core"; + type: "ContractUpgrade"; + chain: number; + address: string; +} + +// Parse a core contract upgrade payload +const coreContractUpgradeParser: P = new P( + new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from("Core").toString("hex").padStart(64, "0"), + formatter: (_str) => "Core", + }) + .uint8("type", { + assert: 1, + formatter: (_action) => "ContractUpgrade", + }) + .uint16("chain") + .array("address", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) +); + +function serialiseCoreContractUpgrade(payload: CoreContractUpgrade): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 1), + encode("uint16", payload.chain), + encode("bytes32", payload.address), + ]; + return body.join(""); +} + +export interface PortalContractUpgrade< + Module extends "NFTBridge" | "TokenBridge" +> { + module: Module; + type: "ContractUpgrade"; + chain: number; + address: string; +} + +// Parse a portal contract upgrade payload +function portalContractUpgradeParser< + Module extends "NFTBridge" | "TokenBridge" +>(module: Module): P> { + return new P( + new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from(module).toString("hex").padStart(64, "0"), + formatter: (_str: string) => module, + }) + .uint8("type", { + assert: 2, + formatter: (_action: number) => "ContractUpgrade", + }) + .uint16("chain") + .array("address", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) + ); +} + +function serialisePortalContractUpgrade< + Module extends "NFTBridge" | "TokenBridge" +>(payload: PortalContractUpgrade): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 2), + encode("uint16", payload.chain), + encode("bytes32", payload.address), + ]; + return body.join(""); +} + +//////////////////////////////////////////////////////////////////////////////// +// Registrations + +export interface PortalRegisterChain< + Module extends "NFTBridge" | "TokenBridge" +> { + module: Module; + type: "RegisterChain"; + chain: number; + emitterChain: number; + emitterAddress: string; +} + +// Parse a portal chain registration payload +function portalRegisterChainParser( + module: Module +): P> { + return new P( + new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from(module).toString("hex").padStart(64, "0"), + formatter: (_str) => module, + }) + .uint8("type", { + assert: 1, + formatter: (_action) => "RegisterChain", + }) + .uint16("chain") + .uint16("emitterChain") + .array("emitterAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) + ); +} + +function serialisePortalRegisterChain< + Module extends "NFTBridge" | "TokenBridge" +>(payload: PortalRegisterChain): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 1), + encode("uint16", payload.chain), + encode("uint16", payload.emitterChain), + encode("bytes32", payload.emitterAddress), + ]; + return body.join(""); +} + +//////////////////////////////////////////////////////////////////////////////// +// Token bridge + +// payload 1 +export interface TokenBridgeTransfer { + module: "TokenBridge"; + type: "Transfer"; + amount: bigint; + tokenAddress: string; + tokenChain: number; + toAddress: string; + chain: number; + fee: bigint; +} + +function tokenBridgeTransferParser(): P { + return new P( + new Parser() + .endianess("big") + .string("module", { + length: (_) => 0, + formatter: (_) => "TokenBridge", + }) + .uint8("type", { + assert: 1, + formatter: (_action) => "Transfer", + }) + .array("amount", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt(), + }) + .array("tokenAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint16("tokenChain") + .array("toAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint16("chain") + .array("fee", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt(), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) + ); +} + +function serialiseTokenBridgeTransfer(payload: TokenBridgeTransfer): string { + const body = [ + encode("uint8", 1), + encode("uint256", payload.amount), + encode("bytes32", hex(payload.tokenAddress)), + encode("uint16", payload.tokenChain), + encode("bytes32", hex(payload.toAddress)), + encode("uint16", payload.chain), + encode("uint256", payload.fee), + ]; + return body.join(""); +} + +// payload 2 +export interface TokenBridgeAttestMeta { + module: "TokenBridge"; + type: "AttestMeta"; + chain: 0; + tokenAddress: string; + tokenChain: number; + decimals: number; + symbol: string; + name: string; +} + +function tokenBridgeAttestMetaParser(): P { + return new P( + new Parser() + .endianess("big") + .string("module", { + length: (_) => 0, + formatter: (_) => "TokenBridge", + }) + .string("chain", { + length: (_) => 0, + formatter: (_) => 0, + }) + .uint8("type", { + assert: 2, + formatter: (_action) => "AttestMeta", + }) + .array("tokenAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint16("tokenChain") + .uint8("decimals") + .array("symbol", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr: Uint8Array) => + Buffer.from(arr).toString( + "utf8", + arr.findIndex((val) => val != 0) + ), + }) + .array("name", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr: Uint8Array) => + Buffer.from(arr).toString( + "utf8", + arr.findIndex((val) => val != 0) + ), + }) + .string("end", { + greedy: true, + assert: (str) => str === "", + }) + ); +} + +function serialiseTokenBridgeAttestMeta( + payload: TokenBridgeAttestMeta +): string { + const body = [ + encode("uint8", 2), + encode("bytes32", hex(payload.tokenAddress)), + encode("uint16", payload.tokenChain), + encode("uint8", payload.decimals), + encode("bytes32", encodeString(payload.symbol)), + encode("bytes32", encodeString(payload.name)), + ]; + return body.join(""); +} + +// payload 3 +export interface TokenBridgeTransferWithPayload { + module: "TokenBridge"; + type: "TransferWithPayload"; + amount: bigint; + tokenAddress: string; + tokenChain: number; + toAddress: string; + chain: number; + fromAddress: string; + payload: string; +} + +function tokenBridgeTransferWithPayloadParser(): P { + return new P( + new Parser() + .endianess("big") + .string("module", { + length: (_) => 0, + formatter: (_) => "TokenBridge", + }) + .uint8("type", { + assert: 3, + formatter: (_action) => "TransferWithPayload", + }) + .array("amount", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt(), + }) + .array("tokenAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint16("tokenChain") + .array("toAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .uint16("chain") + .array("fromAddress", { + type: "uint8", + lengthInBytes: 32, + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + .array("payload", { + type: "uint8", + greedy: true, + readUntil: "eof", + formatter: (arr) => "0x" + Buffer.from(arr).toString("hex"), + }) + ); +} + +function serialiseTokenBridgeTransferWithPayload( + payload: TokenBridgeTransferWithPayload +): string { + const body = [ + encode("uint8", 3), + encode("uint256", payload.amount), + encode("bytes32", hex(payload.tokenAddress)), + encode("uint16", payload.tokenChain), + encode("bytes32", hex(payload.toAddress)), + encode("uint16", payload.chain), + encode("bytes32", hex(payload.fromAddress)), + payload.payload.substring(2), + ]; + return body.join(""); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 () { + // @ts-ignore + 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 +// in, this function just throws. If the enum type is extended with new cases, +// the call to this function will then fail to compile, drawing attention to an +// unhandled case somewhere. +export function impossible(a: never): any { + throw new Error(`Impossible: ${a}`); +} + +//////////////////////////////////////////////////////////////////////////////// +// Encoder utils + +export type Encoding = + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "uint128" + | "uint256" + | "bytes32" + | "address"; + +export function typeWidth(type: Encoding): number { + switch (type) { + case "uint8": + return 1; + case "uint16": + return 2; + case "uint32": + return 4; + case "uint64": + return 8; + case "uint128": + return 16; + case "uint256": + return 32; + case "bytes32": + return 32; + case "address": + return 20; + } +} + +// Couldn't find a satisfactory binary serialisation solution, so we just use +// the ethers library's encoding logic +export function encode(type: Encoding, val: any): string { + // ethers operates on hex strings (sigh) and left pads everything to 32 + // bytes (64 characters). We take last 2*n characters where n is the width + // of the type being serialised in bytes (since a byte is represented as 2 + // digits in hex). + return ethers.utils.defaultAbiCoder + .encode([type], [val]) + .substr(-2 * typeWidth(type)); +} + +// Encode a string as binary left-padded to 32 bytes, represented as a hex +// string (64 chars long) +export function encodeString(str: string): Buffer { + return Buffer.from(Buffer.from(str).toString("hex").padStart(64, "0"), "hex"); +} + +// Turn hex string with potentially missing 0x prefix into Buffer +function hex(x: string): Buffer { + return Buffer.from( + ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2), + "hex" + ); +} diff --git a/sdk/js/src/vaa/governance.ts b/sdk/js/src/vaa/governance.ts new file mode 100644 index 000000000..3874798fa --- /dev/null +++ b/sdk/js/src/vaa/governance.ts @@ -0,0 +1,31 @@ +import { ParsedVaa, parseVaa, SignedVaa } from "./wormhole"; + +export interface Governance { + module: string; + action: number; + chain: number; + orderPayload: Buffer; +} + +export interface ParsedGovernanceVaa extends ParsedVaa, Governance {} + +export function parseGovernanceVaa(vaa: SignedVaa): ParsedGovernanceVaa { + const parsed = parseVaa(vaa); + return { + ...parsed, + ...parseGovernancePayload(parsed.payload), + }; +} + +export function parseGovernancePayload(payload: Buffer): Governance { + const module = payload.subarray(0, 32).toString().replace(/\0/g, ""); + const action = payload.readUInt8(32); + const chain = payload.readUInt16BE(33); + const orderPayload = payload.subarray(35); + return { + module, + action, + chain, + orderPayload, + }; +} diff --git a/sdk/js/src/vaa/index.ts b/sdk/js/src/vaa/index.ts new file mode 100644 index 000000000..8b8f2f643 --- /dev/null +++ b/sdk/js/src/vaa/index.ts @@ -0,0 +1,5 @@ +export * from "./generic"; +export * from "./governance"; +export * from "./nftBridge"; +export * from "./tokenBridge"; +export * from "./wormhole"; diff --git a/sdk/js/src/vaa/nftBridge.ts b/sdk/js/src/vaa/nftBridge.ts new file mode 100644 index 000000000..364995c17 --- /dev/null +++ b/sdk/js/src/vaa/nftBridge.ts @@ -0,0 +1,119 @@ +import { BN } from "@project-serum/anchor"; +import { ParsedGovernanceVaa, parseGovernanceVaa } from "./governance"; +import { + parseTokenBridgeRegisterChainGovernancePayload, + parseTokenBridgeUpgradeContractGovernancePayload, + TokenBridgeRegisterChain, + TokenBridgeUpgradeContract, +} from "./tokenBridge"; +import { ParsedVaa, parseVaa, SignedVaa } from "./wormhole"; + +export enum NftBridgePayload { + Transfer = 1, +} + +export enum NftBridgeGovernanceAction { + RegisterChain = 1, + UpgradeContract = 2, +} + +export interface NftTransfer { + payloadType: NftBridgePayload.Transfer; + tokenAddress: Buffer; + tokenChain: number; + symbol: string; + name: string; + tokenId: bigint; + uri: string; + to: Buffer; + toChain: number; +} + +export function parseNftTransferPayload(payload: Buffer): NftTransfer { + const payloadType = payload.readUInt8(0); + if (payloadType != NftBridgePayload.Transfer) { + throw new Error("not nft bridge transfer VAA"); + } + const tokenAddress = payload.subarray(1, 33); + const tokenChain = payload.readUInt16BE(33); + const symbol = payload.subarray(35, 67).toString().replace(/\0/g, ""); + const name = payload.subarray(67, 99).toString().replace(/\0/g, ""); + const tokenId = BigInt(new BN(payload.subarray(99, 131)).toString()); + const uriLen = payload.readUInt8(131); + const uri = payload.subarray(132, 132 + uriLen).toString(); + const uriEnd = 132 + uriLen; + const to = payload.subarray(uriEnd, uriEnd + 32); + const toChain = payload.readUInt16BE(uriEnd + 32); + return { + payloadType, + tokenAddress, + tokenChain, + name, + symbol, + tokenId, + uri, + to, + toChain, + }; +} + +export interface ParsedNftTransferVaa extends ParsedVaa, NftTransfer {} + +export function parseNftTransferVaa(vaa: SignedVaa): ParsedNftTransferVaa { + const parsed = parseVaa(vaa); + return { + ...parsed, + ...parseNftTransferPayload(parsed.payload), + }; +} + +export interface NftRegisterChain extends TokenBridgeRegisterChain {} +export interface ParsedNftBridgeRegisterChainVaa + extends ParsedGovernanceVaa, + NftRegisterChain {} + +export function parseNftBridgeRegisterChainGovernancePayload( + payload: Buffer +): NftRegisterChain { + return parseTokenBridgeRegisterChainGovernancePayload(payload); +} + +export function parseNftBridgeRegisterChainVaa( + vaa: SignedVaa +): ParsedNftBridgeRegisterChainVaa { + const parsed = parseGovernanceVaa(vaa); + if (parsed.action != NftBridgeGovernanceAction.RegisterChain) { + throw new Error("parsed.action != NftBridgeGovernanceAction.RegisterChain"); + } + return { + ...parsed, + ...parseNftBridgeRegisterChainGovernancePayload(parsed.orderPayload), + }; +} + +export interface NftBridgeUpgradeContract extends TokenBridgeUpgradeContract {} + +export function parseNftBridgeUpgradeContractGovernancePayload( + payload: Buffer +): NftBridgeUpgradeContract { + return parseTokenBridgeUpgradeContractGovernancePayload(payload); +} + +export interface ParsedNftBridgeUpgradeContractVaa + extends ParsedGovernanceVaa, + NftBridgeUpgradeContract {} + +export function parseNftBridgeUpgradeContractVaa( + vaa: SignedVaa +): ParsedNftBridgeUpgradeContractVaa { + const parsed = parseGovernanceVaa(vaa); + if (parsed.action != NftBridgeGovernanceAction.UpgradeContract) { + throw new Error( + "parsed.action != NftBridgeGovernanceAction.UpgradeContract" + ); + } + return { + ...parsed, + ...parseNftBridgeUpgradeContractGovernancePayload(parsed.orderPayload), + }; +} diff --git a/sdk/js/src/vaa/tokenBridge.ts b/sdk/js/src/vaa/tokenBridge.ts new file mode 100644 index 000000000..35562569f --- /dev/null +++ b/sdk/js/src/vaa/tokenBridge.ts @@ -0,0 +1,177 @@ +import { BN } from "@project-serum/anchor"; +import { ParsedGovernanceVaa, parseGovernanceVaa } from "./governance"; +import { ParsedVaa, parseVaa, SignedVaa } from "./wormhole"; + +export enum TokenBridgePayload { + Transfer = 1, + AttestMeta, + TransferWithPayload, +} + +export enum TokenBridgeGovernanceAction { + RegisterChain = 1, + UpgradeContract = 2, +} + +export interface TokenTransfer { + payloadType: + | TokenBridgePayload.Transfer + | TokenBridgePayload.TransferWithPayload; + amount: bigint; + tokenAddress: Buffer; + tokenChain: number; + to: Buffer; + toChain: number; + fee: bigint | null; + fromAddress: Buffer | null; + tokenTransferPayload: Buffer; +} + +export function parseTokenTransferPayload(payload: Buffer): TokenTransfer { + const payloadType = payload.readUInt8(0); + if ( + payloadType != TokenBridgePayload.Transfer && + payloadType != TokenBridgePayload.TransferWithPayload + ) { + throw new Error("not token bridge transfer VAA"); + } + const amount = BigInt(new BN(payload.subarray(1, 33)).toString()); + const tokenAddress = payload.subarray(33, 65); + const tokenChain = payload.readUInt16BE(65); + const to = payload.subarray(67, 99); + const toChain = payload.readUInt16BE(99); + const fee = + payloadType == 1 + ? BigInt(new BN(payload.subarray(101, 133)).toString()) + : null; + const fromAddress = payloadType == 3 ? payload.subarray(101, 133) : null; + const tokenTransferPayload = payload.subarray(133); + return { + payloadType, + amount, + tokenAddress, + tokenChain, + to, + toChain, + fee, + fromAddress, + tokenTransferPayload, + }; +} + +export interface ParsedTokenTransferVaa extends ParsedVaa, TokenTransfer {} + +export function parseTokenTransferVaa(vaa: SignedVaa): ParsedTokenTransferVaa { + const parsed = parseVaa(vaa); + return { + ...parsed, + ...parseTokenTransferPayload(parsed.payload), + }; +} + +export interface AssetMeta { + payloadType: TokenBridgePayload.AttestMeta; + tokenAddress: Buffer; + tokenChain: number; + decimals: number; + symbol: string; + name: string; +} + +export function parseAttestMetaPayload(payload: Buffer): AssetMeta { + const payloadType = payload.readUInt8(0); + if (payloadType != TokenBridgePayload.AttestMeta) { + throw new Error("not token bridge attest meta VAA"); + } + const tokenAddress = payload.subarray(1, 33); + const tokenChain = payload.readUInt16BE(33); + const decimals = payload.readUInt8(35); + const symbol = payload.subarray(36, 68).toString().replace(/\0/g, ""); + const name = payload.subarray(68, 100).toString().replace(/\0/g, ""); + return { + payloadType, + tokenAddress, + tokenChain, + decimals, + symbol, + name, + }; +} + +export interface ParsedAssetMetaVaa extends ParsedVaa, AssetMeta {} +export type ParsedAttestMetaVaa = ParsedAssetMetaVaa; + +export function parseAttestMetaVaa(vaa: SignedVaa): ParsedAssetMetaVaa { + const parsed = parseVaa(vaa); + return { + ...parsed, + ...parseAttestMetaPayload(parsed.payload), + }; +} + +export interface TokenBridgeRegisterChain { + foreignChain: number; + foreignAddress: Buffer; +} + +export function parseTokenBridgeRegisterChainGovernancePayload( + payload: Buffer +): TokenBridgeRegisterChain { + const foreignChain = payload.readUInt16BE(0); + const foreignAddress = payload.subarray(2, 34); + return { + foreignChain, + foreignAddress, + }; +} + +export interface ParsedTokenBridgeRegisterChainVaa + extends ParsedGovernanceVaa, + TokenBridgeRegisterChain {} + +export function parseTokenBridgeRegisterChainVaa( + vaa: SignedVaa +): ParsedTokenBridgeRegisterChainVaa { + const parsed = parseGovernanceVaa(vaa); + if (parsed.action != TokenBridgeGovernanceAction.RegisterChain) { + throw new Error( + "parsed.action != TokenBridgeGovernanceAction.RegisterChain" + ); + } + return { + ...parsed, + ...parseTokenBridgeRegisterChainGovernancePayload(parsed.orderPayload), + }; +} + +export interface TokenBridgeUpgradeContract { + newContract: Buffer; +} + +export function parseTokenBridgeUpgradeContractGovernancePayload( + payload: Buffer +): TokenBridgeUpgradeContract { + const newContract = payload.subarray(0, 32); + return { + newContract, + }; +} + +export interface ParsedTokenBridgeUpgradeContractVaa + extends ParsedGovernanceVaa, + TokenBridgeUpgradeContract {} + +export function parseTokenBridgeUpgradeContractVaa( + vaa: SignedVaa +): ParsedTokenBridgeUpgradeContractVaa { + const parsed = parseGovernanceVaa(vaa); + if (parsed.action != TokenBridgeGovernanceAction.UpgradeContract) { + throw new Error( + "parsed.action != TokenBridgeGovernanceAction.UpgradeContract" + ); + } + return { + ...parsed, + ...parseTokenBridgeUpgradeContractGovernancePayload(parsed.orderPayload), + }; +} diff --git a/sdk/js/src/vaa/wormhole.ts b/sdk/js/src/vaa/wormhole.ts new file mode 100644 index 000000000..4526ef530 --- /dev/null +++ b/sdk/js/src/vaa/wormhole.ts @@ -0,0 +1,56 @@ +import { keccak256 } from "../utils"; + +export { isBytes } from "ethers/lib/utils"; + +export interface GuardianSignature { + index: number; + signature: Buffer; +} + +export interface ParsedVaa { + version: number; + guardianSetIndex: number; + guardianSignatures: GuardianSignature[]; + timestamp: number; + nonce: number; + emitterChain: number; + emitterAddress: Buffer; + sequence: bigint; + consistencyLevel: number; + payload: Buffer; + hash: Buffer; +} + +export type SignedVaa = Uint8Array | Buffer; + +export function parseVaa(vaa: SignedVaa): ParsedVaa { + const signedVaa = Buffer.isBuffer(vaa) ? vaa : Buffer.from(vaa as Uint8Array); + const sigStart = 6; + const numSigners = signedVaa[5]; + const sigLength = 66; + + const guardianSignatures: GuardianSignature[] = []; + for (let i = 0; i < numSigners; ++i) { + const start = sigStart + i * sigLength; + guardianSignatures.push({ + index: signedVaa[start], + signature: signedVaa.subarray(start + 1, start + 66), + }); + } + + const body = signedVaa.subarray(sigStart + sigLength * numSigners); + + return { + version: signedVaa[0], + guardianSetIndex: signedVaa.readUInt32BE(1), + guardianSignatures, + timestamp: body.readUInt32BE(0), + nonce: body.readUInt32BE(4), + emitterChain: body.readUInt16BE(8), + emitterAddress: body.subarray(10, 42), + sequence: body.readBigUInt64BE(42), + consistencyLevel: body[50], + payload: body.subarray(51), + hash: keccak256(body), + }; +} diff --git a/sdk/js/tsconfig.json b/sdk/js/tsconfig.json index 4cd027d9a..284390763 100644 --- a/sdk/js/tsconfig.json +++ b/sdk/js/tsconfig.json @@ -9,6 +9,7 @@ "esModuleInterop": true, "downlevelIteration": true, "allowJs": true, + "resolveJsonModule": true, "lib": ["dom", "es5", "scripthost", "es2020.bigint"] }, "include": ["src", "types"], diff --git a/solana/idl/nft_bridge.json b/solana/idl/nft_bridge.json new file mode 100644 index 000000000..2a06bda19 --- /dev/null +++ b/solana/idl/nft_bridge.json @@ -0,0 +1,621 @@ +{ + "version": "0.1.0", + "name": "wormhole", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "wormhole", + "type": "publicKey" + } + ] + }, + { + "name": "completeNative", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "to", + "isMut": true, + "isSigner": false + }, + { + "name": "toAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "custodySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "completeWrapped", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "to", + "isMut": true, + "isSigner": false + }, + { + "name": "toAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": true, + "isSigner": false + }, + { + "name": "mintAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "completeWrappedMeta", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadata", + "isMut": true, + "isSigner": false + }, + { + "name": "mintAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferWrapped", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "fromOwner", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + } + ] + }, + { + "name": "transferNative", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "splMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "custodySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + } + ] + }, + { + "name": "registerChain", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "upgradeContract", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "spill", + "isMut": true, + "isSigner": false + }, + { + "name": "implementation", + "isMut": true, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "nftBridgeProgram", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeable", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + + ] +} diff --git a/solana/idl/token_bridge.json b/solana/idl/token_bridge.json new file mode 100644 index 000000000..b998ed21e --- /dev/null +++ b/solana/idl/token_bridge.json @@ -0,0 +1,947 @@ +{ + "version": "0.1.0", + "name": "wormhole", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "wormhole", + "type": "publicKey" + } + ] + }, + { + "name": "attestToken", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + } + ] + }, + { + "name": "completeNative", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "to", + "isMut": true, + "isSigner": false + }, + { + "name": "toFees", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "custodySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "completeWrapped", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "to", + "isMut": true, + "isSigner": false + }, + { + "name": "toFees", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "mintAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferWrapped", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "fromOwner", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + } + ] + }, + { + "name": "transferNative", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "custodySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + } + ] + }, + { + "name": "registerChain", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "createWrapped", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "endpoint", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": true, + "isSigner": false + }, + { + "name": "splMetadata", + "isMut": true, + "isSigner": false + }, + { + "name": "mintAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "upgradeContract", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "spill", + "isMut": true, + "isSigner": false + }, + { + "name": "implementation", + "isMut": true, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenBridgeProgram", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeable", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferWrappedWithPayload", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "fromOwner", + "isMut": true, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "wrappedMeta", + "isMut": false, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "cpiProgramId", + "type": { + "option": "publicKey" + } + } + ] + }, + { + "name": "transferNativeWithPayload", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + }, + { + "name": "authoritySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "custodySigner", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeBridge", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "wormholeEmitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeSequence", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeFeeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "targetAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "targetChain", + "type": "u16" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "cpiProgramId", + "type": { + "option": "publicKey" + } + } + ] + } + ], + "accounts": [ + + ] +} diff --git a/solana/idl/wormhole.json b/solana/idl/wormhole.json new file mode 100644 index 000000000..a60f24b9a --- /dev/null +++ b/solana/idl/wormhole.json @@ -0,0 +1,624 @@ +{ + "version": "0.1.0", + "name": "wormhole", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "guardianSetExpirationTime", + "type": "u32" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "initialGuardians", + "type": { + "vec": { + "array": [ + "u8", + 20 + ] + } + } + } + ] + }, + { + "name": "postMessage", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "message", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": true + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "consistencyLevel", + "type": "u8" + } + ] + }, + { + "name": "postVaa", + "accounts": [ + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + }, + { + "name": "bridge", + "isMut": false, + "isSigner": false + }, + { + "name": "signatureSet", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "guardianSetIndex", + "type": "u32" + }, + { + "name": "timestamp", + "type": "u32" + }, + { + "name": "nonce", + "type": "u32" + }, + { + "name": "emitterChain", + "type": "u16" + }, + { + "name": "emitterAddress", + "type": { + "array": [ + "u8", 32 + ] + } + }, + { + "name": "sequence", + "type": "u64" + }, + { + "name": "consistencyLevel", + "type": "u8" + }, + { + "name": "payload", + "type": "bytes" + } + ] + }, + { + "name": "setFees", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferFees", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "upgradeContract", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "spill", + "isMut": true, + "isSigner": false + }, + { + "name": "implementation", + "isMut": true, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "wormholeProgram", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeable", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "upgradeGuardianSet", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "claim", + "isMut": true, + "isSigner": false + }, + { + "name": "guardianSetOld", + "isMut": true, + "isSigner": false + }, + { + "name": "guardianSetNew", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "verifySignatures", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + }, + { + "name": "signatureSet", + "isMut": true, + "isSigner": true + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "signatureStatus", + "type": { + "array": [ + "i8", 19 + ] + } + } + ] + }, + { + "name": "postMessageUnreliable", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "message", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": true + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "consistencyLevel", + "type": "u8" + } + ] + } + ], + "accounts": [ + { + "name": "PostedMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaaVersion", + "type": "u8" + }, + { + "name": "consistencyLevel", + "type": "u8" + }, + { + "name": "vaaTime", + "type": "u32" + }, + { + "name": "vaaSignatureAccount", + "type": "publicKey" + }, + { + "name": "submissionTime", + "type": "u32" + }, + { + "name": "nonce", + "type": "u32" + }, + { + "name": "sequence", + "type": "u64" + }, + { + "name": "emitterChain", + "type": "u16" + }, + { + "name": "emitterAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "payload", + "type": "bytes" + } + ] + } + }, + { + "name": "PostedVAA", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaaVersion", + "type": "u8" + }, + { + "name": "consistencyLevel", + "type": "u8" + }, + { + "name": "vaaTime", + "type": "u32" + }, + { + "name": "vaaSignatureAccount", + "type": "publicKey" + }, + { + "name": "submissionTime", + "type": "u32" + }, + { + "name": "nonce", + "type": "u32" + }, + { + "name": "sequence", + "type": "u64" + }, + { + "name": "emitterChain", + "type": "u16" + }, + { + "name": "emitterAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "payload", + "type": "bytes" + } + ] + } + } + ] +} diff --git a/testing/Dockerfile.sdk.test b/testing/Dockerfile.sdk.test index c0045b7db..9410ac40e 100644 --- a/testing/Dockerfile.sdk.test +++ b/testing/Dockerfile.sdk.test @@ -10,6 +10,8 @@ RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \ npm ci --prefix ethereum COPY ethereum ./ethereum +COPY solana/idl ./solana/idl/ + COPY sdk/js/package.json sdk/js/package-lock.json ./sdk/js/ RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \ npm ci --prefix sdk/js diff --git a/testing/solana-test-validator/.gitignore b/testing/solana-test-validator/.gitignore new file mode 100644 index 000000000..14647191b --- /dev/null +++ b/testing/solana-test-validator/.gitignore @@ -0,0 +1,3 @@ +.test +artifacts +node_modules diff --git a/testing/solana-test-validator/Makefile b/testing/solana-test-validator/Makefile new file mode 100644 index 000000000..7b56c460a --- /dev/null +++ b/testing/solana-test-validator/Makefile @@ -0,0 +1,21 @@ +.PHONY: clean all help .FORCE + +-include ../Makefile.help + +.FORCE: + +node_modules: + yarn + +artifacts: node_modules + cd ../../solana && DOCKER_BUILDKIT=1 docker build -f Dockerfile --build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC -o ../testing/solana-test-validator/artifacts . + +.PHONY: test +test: artifacts + @echo "Running integration tests" + yarn run sdk-tests + +.PHONY: clean +clean: + rm -rf artifacts node_modules validator.log .test + diff --git a/testing/solana-test-validator/package.json b/testing/solana-test-validator/package.json new file mode 100644 index 000000000..6c77e4e67 --- /dev/null +++ b/testing/solana-test-validator/package.json @@ -0,0 +1,28 @@ +{ + "private": true, + "scripts": { + "sdk-tests": "bash run_sdk_tests.sh", + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@project-serum/anchor": "0.25.0", + "@solana/spl-token": "^0.3.1", + "@solana/web3.js": "^1.53.0", + "byteify": "^2.0.10", + "camelcase": "^7.0.0", + "elliptic": "^6.5.4", + "keccak256": "^1.0.6" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.1.1", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "ts-node": "^10.9.1", + "typescript": "^4.3.5" + } +} diff --git a/testing/solana-test-validator/run_sdk_tests.sh b/testing/solana-test-validator/run_sdk_tests.sh new file mode 100755 index 000000000..65f215859 --- /dev/null +++ b/testing/solana-test-validator/run_sdk_tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +### maybe a validator is already running +pgrep -f solana-test-validator +if [ $? -eq 0 ]; then + echo "solana-test-validator already running" + exit 1; +fi + +ROOT=$(dirname $0) + +### prepare local validator +ARTIFACTS=$ROOT/artifacts +ACCOUNTS=$ROOT/sdk-tests/accounts +TEST=$ROOT/.test + +solana-test-validator --reset \ + --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s $ARTIFACTS/spl_token_metadata.so \ + --ledger $TEST > validator.log 2>&1 & +sleep 2 + +### write program logs +PROGRAM_LOGS=$TEST/program-logs +mkdir -p $PROGRAM_LOGS + +RPC=http://localhost:8899 +solana logs agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC --url $RPC > $PROGRAM_LOGS/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC & +solana logs bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa --url $RPC > $PROGRAM_LOGS/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa & +solana logs caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q --url $RPC > $PROGRAM_LOGS/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q & +solana logs KeccakSecp256k11111111111111111111111111111 --url $RPC > $PROGRAM_LOGS/KeccakSecp256k11111111111111111111111111111 & +solana logs metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s --url $RPC > $PROGRAM_LOGS/metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s & + +### run tests +yarn run ts-mocha -p ./tsconfig.json -t 1000000 sdk-tests/*.ts + +### nuke +pkill -f "solana logs" +pkill -f solana-test-validator diff --git a/testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts b/testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts new file mode 100644 index 000000000..7ef9f3155 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/0_deploy_and_upgrade.ts @@ -0,0 +1,361 @@ +import { expect } from "chai"; +import * as web3 from "@solana/web3.js"; +import fs from "fs"; +import { MockGuardians, GovernanceEmitter } from "../../../sdk/js/src/mock"; +import { + getGuardianSet, + getWormholeBridgeData, + createInitializeInstruction as createWormholeInitializeInstruction, + deriveUpgradeAuthorityKey, + createUpgradeContractInstruction as createWormholeUpgradeContractInstruction, +} from "../../../sdk/js/src/solana/wormhole"; +import { + createInitializeInstruction as createTokenBridgeInitializeInstruction, + createUpgradeContractInstruction as createTokenBridgeUpgradeContractInstruction, + getTokenBridgeConfig, +} from "../../../sdk/js/src/solana/tokenBridge"; +import { + createInitializeInstruction as createNftBridgeInitializeInstruction, + createUpgradeContractInstruction as createNftBridgeUpgradeContractInstruction, +} from "../../../sdk/js/src/solana/nftBridge"; +import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa"; +import { NodeWallet } from "../../../sdk/js/src/solana/utils"; + +import { + CORE_BRIDGE_ADDRESS, + GOVERNANCE_EMITTER_ADDRESS, + GUARDIAN_KEYS, + GUARDIAN_SET_INDEX, + LOCALHOST, + NFT_BRIDGE_ADDRESS, + TOKEN_BRIDGE_ADDRESS, +} from "./helpers/consts"; +import { + deployProgram, + execSolanaWriteBufferAndSetBufferAuthority, +} from "./helpers/utils"; +import { getNftBridgeConfig } from "../../../sdk/js/src/solana/nftBridge"; + +describe("Deploy and Upgrade Programs", () => { + const connection = new web3.Connection(LOCALHOST, "processed"); + + const payerPath = `${__dirname}/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json`; + const wallet = NodeWallet.fromSecretKey( + Uint8Array.from( + JSON.parse( + fs.readFileSync(payerPath, { + encoding: "utf8", + }) + ) + ) + ); + + // for signing wormhole messages + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); + + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex") + ); + + const localVariables: any = {}; + + before("Airdrop SOL", async () => { + // wallet + await connection + .requestAirdrop(wallet.key(), 1000 * web3.LAMPORTS_PER_SOL) + .then(async (signature) => connection.confirmTransaction(signature)); + }); + + describe("Wormhole (Core Bridge)", () => { + it("Deploy and Initialize", async () => { + const artifactPath = `${__dirname}/../artifacts/bridge.so`; + const programIdPath = `${__dirname}/keys/${CORE_BRIDGE_ADDRESS}.json`; + const upgradeAuthority = deriveUpgradeAuthorityKey(CORE_BRIDGE_ADDRESS); + + deployProgram( + payerPath, + artifactPath, + programIdPath, + CORE_BRIDGE_ADDRESS, + upgradeAuthority + ); + + // initialize + const guardianSetExpirationTime = 86400; + const fee = 100n; + const initialGuardians = guardians.getPublicKeys().slice(0, 1); + + const initializeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createWormholeInitializeInstruction( + CORE_BRIDGE_ADDRESS, + wallet.key(), + guardianSetExpirationTime, + fee, + initialGuardians + ) + ), + [wallet.signer()] + ); + // console.log(`initializeTx: ${initializeTx}`); + + // verify data + const info = await getWormholeBridgeData(connection, CORE_BRIDGE_ADDRESS); + expect(info.guardianSetIndex).to.equal(GUARDIAN_SET_INDEX); + expect(info.config.guardianSetExpirationTime).to.equal( + guardianSetExpirationTime + ); + expect(info.config.fee).to.equal(fee); + const guardianSet = await getGuardianSet( + connection, + CORE_BRIDGE_ADDRESS, + 0 + ); + expect(guardianSet.index).to.equal(GUARDIAN_SET_INDEX); + expect(guardianSet.expirationTime).to.equal(0); + expect(guardianSet.keys).has.length(1); + expect( + Buffer.compare(initialGuardians[0], guardianSet.keys.at(0)!) + ).to.equal(0); + }); + + it("Upgrade Contract", async () => { + // first upload BPF of implementation + const artifactPath = `${__dirname}/../artifacts/bridge.so`; + const upgradeAuthority = deriveUpgradeAuthorityKey(CORE_BRIDGE_ADDRESS); + + const implementation = execSolanaWriteBufferAndSetBufferAuthority( + payerPath, + artifactPath, + upgradeAuthority + ); + + // now pass implementation through governance + const timestamp = 1; + const chain = 1; + const message = governance.publishWormholeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const upgradeContractIx = createWormholeUpgradeContractInstruction( + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const upgradeContractTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(upgradeContractIx), + [wallet.signer()] + ); + // console.log(`upgradeContractTx: ${upgradeContractTx}`); + + // not sure what to verify. if there are no errors, + // transaction was successful and the upgrade test passes + }); + }); + + describe("Token Bridge", () => { + it("Deploy and Initialize", async () => { + const artifactPath = `${__dirname}/../artifacts/token_bridge.so`; + const programIdPath = `${__dirname}/keys/${TOKEN_BRIDGE_ADDRESS}.json`; + const upgradeAuthority = deriveUpgradeAuthorityKey(TOKEN_BRIDGE_ADDRESS); + + deployProgram( + payerPath, + artifactPath, + programIdPath, + TOKEN_BRIDGE_ADDRESS, + upgradeAuthority + ); + + // we will initialize using CORE_BRIDGE_ADDRESS instead of + // UPGRADEABLE_CORE_BRIDGE_ADDRESS because the Wormhole owner is only + // valid for CORE_BRIDGE_ADDRESS with the bpf we deployed + // + // AccountOwner::Other(Pubkey::from_str(env!("BRIDGE_ADDRESS")).unwrap()) + const initializeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createTokenBridgeInitializeInstruction( + TOKEN_BRIDGE_ADDRESS, + wallet.key(), + CORE_BRIDGE_ADDRESS + ) + ), + [wallet.signer()] + ); + // console.log(`initializeTx: ${initializeTx}`); + + // verify data + const config = await getTokenBridgeConfig( + connection, + TOKEN_BRIDGE_ADDRESS + ); + expect(config.wormhole.equals(CORE_BRIDGE_ADDRESS)).to.be.true; + }); + + it("Upgrade Contract", async () => { + // first upload BPF of implementation + const artifactPath = `${__dirname}/../artifacts/token_bridge.so`; + const upgradeAuthority = deriveUpgradeAuthorityKey(TOKEN_BRIDGE_ADDRESS); + + const implementation = execSolanaWriteBufferAndSetBufferAuthority( + payerPath, + artifactPath, + upgradeAuthority + ); + + // now pass implementation through governance + const timestamp = 2; + const chain = 1; + const message = governance.publishTokenBridgeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const upgradeContractIx = createTokenBridgeUpgradeContractInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const upgradeContractTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(upgradeContractIx), + [wallet.signer()] + ); + // console.log(`upgradeContractTx: ${upgradeContractTx}`); + + // not sure what to verify. if there are no errors, + // transaction was successful and the upgrade test passes + }); + }); + + describe("NFT Bridge", () => { + it("Deploy and Initialize", async () => { + const artifactPath = `${__dirname}/../artifacts/nft_bridge.so`; + const programIdPath = `${__dirname}/keys/${NFT_BRIDGE_ADDRESS}.json`; + const upgradeAuthority = deriveUpgradeAuthorityKey(NFT_BRIDGE_ADDRESS); + + deployProgram( + payerPath, + artifactPath, + programIdPath, + NFT_BRIDGE_ADDRESS, + upgradeAuthority + ); + + // we will initialize using CORE_BRIDGE_ADDRESS instead of + // UPGRADEABLE_CORE_BRIDGE_ADDRESS because the Wormhole owner is only + // valid for CORE_BRIDGE_ADDRESS with the bpf we deployed + // + // AccountOwner::Other(Pubkey::from_str(env!("BRIDGE_ADDRESS")).unwrap()) + const initializeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createNftBridgeInitializeInstruction( + NFT_BRIDGE_ADDRESS, + wallet.key(), + CORE_BRIDGE_ADDRESS + ) + ), + [wallet.signer()] + ); + // console.log(`initializeTx: ${initializeTx}`); + + // verify data + const config = await getNftBridgeConfig(connection, NFT_BRIDGE_ADDRESS); + expect(config.wormhole.equals(CORE_BRIDGE_ADDRESS)).to.be.true; + }); + + it("Upgrade Contract", async () => { + // first upload BPF of implementation + const artifactPath = `${__dirname}/../artifacts/nft_bridge.so`; + const upgradeAuthority = deriveUpgradeAuthorityKey(NFT_BRIDGE_ADDRESS); + + const implementation = execSolanaWriteBufferAndSetBufferAuthority( + payerPath, + artifactPath, + upgradeAuthority + ); + + // now pass implementation through governance + const timestamp = 3; + const chain = 1; + const message = governance.publishNftBridgeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const upgradeContractIx = createNftBridgeUpgradeContractInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const upgradeContractTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(upgradeContractIx), + [wallet.signer()] + ); + // console.log(`upgradeContractTx: ${upgradeContractTx}`); + + // not sure what to verify. if there are no errors, + // transaction was successful and the upgrade test passes + }); + }); +}); diff --git a/testing/solana-test-validator/sdk-tests/1_wormhole.ts b/testing/solana-test-validator/sdk-tests/1_wormhole.ts new file mode 100644 index 000000000..d61970f02 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/1_wormhole.ts @@ -0,0 +1,760 @@ +import { expect } from "chai"; +import * as web3 from "@solana/web3.js"; +import { + MockGuardians, + MockEthereumEmitter, + GovernanceEmitter, +} from "../../../sdk/js/src/mock"; +import { parseVaa, parseGovernanceVaa } from "../../../sdk/js/src/vaa"; +import { + getPostedVaa, + getGuardianSet, + createSetFeesInstruction, + createTransferFeesInstruction, + createUpgradeGuardianSetInstruction, + getWormholeBridgeData, + getInitializeAccounts, + getPostMessageAccounts, + getPostVaaAccounts, + getSetFeesAccounts, + getTransferFeesAccounts, + getUpgradeGuardianSetAccounts, + getVerifySignatureAccounts, + getUpgradeContractAccounts, + getSignatureSetData, +} from "../../../sdk/js/src/solana/wormhole"; +import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa"; +import { + BpfLoaderUpgradeable, + NodeWallet, + getPostMessageCpiAccounts, +} from "../../../sdk/js/src/solana"; + +import { + CORE_BRIDGE_ADDRESS, + ETHEREUM_WALLET_BYTES32, + GOVERNANCE_EMITTER_ADDRESS, + GUARDIAN_KEYS, + GUARDIAN_SET_INDEX, + LOCALHOST, + TOKEN_BRIDGE_ADDRESS, +} from "./helpers/consts"; + +describe("Wormhole (Core Bridge)", () => { + const connection = new web3.Connection(LOCALHOST, "processed"); + + const wallet = new NodeWallet(web3.Keypair.generate()); + + // for signing wormhole messages + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); + + const localVariables: any = {}; + + before("Airdrop SOL", async () => { + await connection + .requestAirdrop(wallet.key(), 1000 * web3.LAMPORTS_PER_SOL) + .then(async (signature) => connection.confirmTransaction(signature)); + }); + + describe("Accounts", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex") + ); + + // hijacking the ethereum token bridge address for our fake emitter + const ethereumWormhole = new MockEthereumEmitter(ETHEREUM_WALLET_BYTES32); + + const payer = new web3.PublicKey( + "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" + ); + + it("Instruction 1: Initialize", () => { + const accounts = getInitializeAccounts(CORE_BRIDGE_ADDRESS, payer); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.guardianSet.toString()).to.equal( + "BJmSHooX4QJCTE4bn5G2Pv6in1nLGyWvL3jxWmT5Avdm" + ); + expect(accounts.feeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 2: Post Message", () => { + const message = web3.Keypair.generate(); + + const accounts = getPostMessageAccounts( + CORE_BRIDGE_ADDRESS, + payer, + TOKEN_BRIDGE_ADDRESS, + message.publicKey + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.message.equals(message.publicKey)).is.true; + expect(accounts.emitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.sequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.feeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 3: Post VAA", () => { + const message = Buffer.from("All your base are belong to us."); + const nonce = 0; + const consistencyLevel = 15; + const timestamp = 12345678; + const published = ethereumWormhole.publishMessage( + nonce, + message, + consistencyLevel, + timestamp + ); + const signedVaa = guardians.addSignatures(published, [0]); + + const signatureSet = web3.Keypair.generate(); + const accounts = getPostVaaAccounts( + CORE_BRIDGE_ADDRESS, + payer, + signatureSet.publicKey, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.guardianSet.toString()).to.equal( + "BJmSHooX4QJCTE4bn5G2Pv6in1nLGyWvL3jxWmT5Avdm" + ); + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.signatureSet.equals(signatureSet.publicKey)).is.true; + expect(accounts.vaa.toString()).to.equal( + "5UfHDKqHwQnMtHjnqfpZJxmAeCyMWD7kYEPcfeKQwvRY" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 4: Set Fees", () => { + const timestamp = 23456789; + const newFeeAmount = 42069n; + const message = governance.publishWormholeSetMessageFee( + timestamp, + 1, + newFeeAmount + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getSetFeesAccounts( + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.vaa.toString()).to.equal( + "Bfon9eTUxC8t9PfryWKChbcVnYGUu7nzTXNaY16DNWEM" + ); + expect(accounts.claim.toString()).to.equal( + "BgMQaDvs4m9B2NMPbvjRw3VedZ3nnR3E77Cd5cJ8EjV9" + ); + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 5: Transfer Fees", () => { + const timestamp = 34567890; + const chain = 1; + const amount = 0n; + const recipient = payer; + const message = governance.publishWormholeTransferFees( + timestamp, + chain, + amount, + recipient.toBuffer() + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getTransferFeesAccounts( + CORE_BRIDGE_ADDRESS, + payer, + recipient, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.vaa.toString()).to.equal( + "CTGU5aCPifz9JXveMKjXYqXNPPTx6n49XDEzffxhth15" + ); + expect(accounts.claim.toString()).to.equal( + "9cR8vkCTqSctEREA8yjayLbaDr8FGjnuQFF5ABNVQzzR" + ); + expect(accounts.feeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 6: Upgrade Contract", () => { + const timestamp = 45678901; + const chain = 1; + const implementation = new web3.PublicKey( + "2B5wMnErS8oKWV1wPTNQQhM1WLyxee2obtBMDtsYeHgA" + ); + const message = governance.publishWormholeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getUpgradeContractAccounts( + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.vaa.toString()).to.equal( + "8Paf1ZasFS8EoJJfaZASChsHS77pm6LvFUypAbRmPYhZ" + ); + expect(accounts.claim.toString()).to.equal( + "3mVQfmBT2g933Bm3yTVtd5Yz4PQkLPMnTgrmkpShAeXM" + ); + expect(accounts.upgradeAuthority.toString()).to.equal( + "2Esys2cab9dkWeApHewy7nqx6tNUWKGtFchyhRpzGmR6" + ); + expect(accounts.spill.equals(payer)).is.true; + expect(accounts.implementation.equals(implementation)).is.true; + expect(accounts.programData.toString()).to.equal( + "Bi88esKkELCqVAYUFjREwnWjeK4RUWecHv7VxZQtVj4f" + ); + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect( + accounts.bpfLoaderUpgradeable.equals(BpfLoaderUpgradeable.programId) + ).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 7: Upgrade Guardian Set", () => { + const timestamp = 56789012; + const newGuardianSetIndex = guardians.setIndex + 1; + const newGuardianSet = guardians.getPublicKeys(); + const message = governance.publishWormholeGuardianSetUpgrade( + timestamp, + newGuardianSetIndex, + newGuardianSet + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getUpgradeGuardianSetAccounts( + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.bridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.vaa.toString()).to.equal( + "DeisQv7bpLMenGLiqWGmkdVnyXrfbMmttf9iVXsWsSDg" + ); + expect(accounts.claim.toString()).to.equal( + "3up9EcEUXnxkiBfdxBTfK4FuJagHXbzHFbn7uXLWwqt4" + ); + expect(accounts.guardianSetOld.toString()).to.equal( + "BJmSHooX4QJCTE4bn5G2Pv6in1nLGyWvL3jxWmT5Avdm" + ); + expect(accounts.guardianSetNew.toString()).to.equal( + "mp6ZSV2cM3B8YHuySMsn4yJzpjPG2YLMvqtfGtEMxRX" + ); + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 8: Verify Signatures", () => { + const timestamp = 67890123; + const message = Buffer.from("All your base are belong to us."); + const nonce = 0; + const consistencyLevel = 15; + const published = ethereumWormhole.publishMessage( + nonce, + message, + consistencyLevel, + timestamp + ); + const signedVaa = guardians.addSignatures(published, [0]); + + const signatureSet = web3.Keypair.generate(); + const accounts = getVerifySignatureAccounts( + CORE_BRIDGE_ADDRESS, + payer, + signatureSet.publicKey, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.guardianSet.toString()).to.equal( + "BJmSHooX4QJCTE4bn5G2Pv6in1nLGyWvL3jxWmT5Avdm" + ); + expect(accounts.signatureSet.equals(signatureSet.publicKey)).is.true; + expect(accounts.instructions.equals(web3.SYSVAR_INSTRUCTIONS_PUBKEY)).to + .be.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + }); + + describe("CPI Accounts", () => { + const payer = new web3.PublicKey( + "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" + ); + + // mock program integrating wormhole + const cpiProgramId = new web3.PublicKey( + "pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ" + ); + + it("getPostMessageCpiAccounts", () => { + const message = web3.Keypair.generate(); + + const accounts = getPostMessageCpiAccounts( + cpiProgramId, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeMessage.equals(message.publicKey)).is.true; + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ernk5wzhwTPJDbmTNnELqhxW5J85CH45qJSTsGkKpGYK" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "5w3YWnJUVbuDvBpymrsu6oecpwY17n82Nw9b9qXZ1z6m" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + }); + + describe("Wormhole Program Interaction", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), + 10 + ); + + // hijacking the ethereum token bridge address for our fake emitter + const ethereumWormhole = new MockEthereumEmitter(ETHEREUM_WALLET_BYTES32); + + describe("Post VAA with One Guardian", () => { + it("Verify Guardian Signature and Post Message", async () => { + const message = Buffer.from("All your base are belong to us."); + const nonce = 0; + const consistencyLevel = 15; + const timestamp = 12345678; + const published = ethereumWormhole.publishMessage( + nonce, + message, + consistencyLevel, + timestamp + ); + const signingGuardians = [0]; + const signedVaa = guardians.addSignatures(published, signingGuardians); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + // verify data + const parsed = parseVaa(signedVaa); + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parsed.hash + ).then((postedVaa) => postedVaa.message); + + expect(messageData.consistencyLevel).to.equal(consistencyLevel); + expect(messageData.consistencyLevel).to.equal(parsed.consistencyLevel); + expect( + Buffer.compare(messageData.emitterAddress, parsed.emitterAddress) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(parsed.emitterChain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.nonce).to.equal(parsed.nonce); + expect(Buffer.compare(messageData.payload, message)).to.equal(0); + expect(Buffer.compare(messageData.payload, parsed.payload)).to.equal(0); + expect(messageData.sequence).to.equal(parsed.sequence); + expect(messageData.vaaTime).to.equal(timestamp); + expect(messageData.vaaTime).to.equal(parsed.timestamp); + expect(messageData.vaaVersion).to.equal(parsed.version); + + const signatureSetData = await getSignatureSetData( + connection, + messageData.vaaSignatureAccount + ); + const signed = signatureSetData.signatures; + expect(signed).has.length(1); + expect(signed.filter((x) => !x)).has.length(0); + for (const i of signingGuardians) { + expect(signed[i]).is.true; + } + expect(Buffer.compare(signatureSetData.hash, parsed.hash)).to.equal(0); + expect(signatureSetData.guardianSetIndex).to.equal(guardians.setIndex); + }); + + // it("Post Message Unreliable", () => { + // // jk + // }); + }); + + describe("Governance", () => { + it("Set Fees to Arbitrary Amount", async () => { + const previousFee = await getWormholeBridgeData( + connection, + CORE_BRIDGE_ADDRESS + ).then((info) => info.config.fee); + + const timestamp = 1; + const newFeeAmount = previousFee + BigInt(69420); + const message = governance.publishWormholeSetMessageFee( + timestamp, + 1, + newFeeAmount + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const setFeeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createSetFeesInstruction( + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ) + ), + [wallet.signer()] + ); + // console.log(`setFeeTx: ${setFeeTx}`); + + const currentFee = await getWormholeBridgeData( + connection, + CORE_BRIDGE_ADDRESS + ).then((info) => info.config.fee); + expect(currentFee).to.equal(newFeeAmount); + }); + + // this test is a little silly because we will not have had anyone using + // the core bridge where someone will have paid fees. So we just demonstrate + // that the instruction works by sending 0 lamports to an arbitrary recipient + it("Transfer Fees to Recipient", async () => { + const recipient = web3.Keypair.generate().publicKey; + //const balanceBefore = await connection.getBalance(recipient); + + const timestamp = 2; + const chain = 1; + const amount = 0n; + const message = governance.publishWormholeTransferFees( + timestamp, + chain, + amount, + recipient.toBuffer() + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const transferFeeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createTransferFeesInstruction( + CORE_BRIDGE_ADDRESS, + wallet.key(), + recipient, + signedVaa + ) + ), + [wallet.signer()] + ); + // console.log(`transferFeeTx: ${transferFeeTx}`); + + //const balanceAfter = await connection.getBalance(recipient); + }); + + it("Upgrade Guardian Set to 19 Guardians", async () => { + const timestamp = 3; + const newGuardianSetIndex = guardians.setIndex + 1; + const newGuardianSet = guardians.getPublicKeys(); + const message = governance.publishWormholeGuardianSetUpgrade( + timestamp, + newGuardianSetIndex, + newGuardianSet + ); + const signedVaa = guardians.addSignatures(message, [0]); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const parsed = parseGovernanceVaa(signedVaa); + const upgradeTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createUpgradeGuardianSetInstruction( + CORE_BRIDGE_ADDRESS, + wallet.key(), + parsed + ) + ), + [wallet.signer()] + ); + // console.log(`upgradeGuardianSet: ${upgradeTx}`); + + // update guardian's set index now and verify upgrade + guardians.updateGuardianSetIndex(newGuardianSetIndex); + + const guardianSetData = await getGuardianSet( + connection, + CORE_BRIDGE_ADDRESS, + newGuardianSetIndex + ); + expect(guardianSetData.index).to.equal(newGuardianSetIndex); + expect(guardianSetData.creationTime).to.equal(parsed.timestamp); + for (let i = 0; i < newGuardianSet.length; ++i) { + const key = guardianSetData.keys.at(i)!; + const expectedKey = newGuardianSet.at(i)!; + expect(Buffer.compare(key, expectedKey)).to.equal(0); + } + }); + }); + + describe("Post VAA with 19 Guardians", () => { + it("Post VAA Signed with 13 Guardians", async () => { + const message = Buffer.from("All your base are belong to us."); + const nonce = 0; + const consistencyLevel = 15; + const published = ethereumWormhole.publishMessage( + nonce, + message, + consistencyLevel + ); + const signingGuardians = [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18]; + const signedVaa = guardians.addSignatures(published, signingGuardians); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + // verify data + const parsed = parseVaa(signedVaa); + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parsed.hash + ).then((postedVaa) => postedVaa.message); + expect(messageData.consistencyLevel).to.equal(consistencyLevel); + expect(messageData.consistencyLevel).to.equal(parsed.consistencyLevel); + expect( + Buffer.compare(messageData.emitterAddress, parsed.emitterAddress) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(parsed.emitterChain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.nonce).to.equal(parsed.nonce); + expect(Buffer.compare(messageData.payload, message)).to.equal(0); + expect(Buffer.compare(messageData.payload, parsed.payload)).to.equal(0); + expect(messageData.sequence).to.equal(parsed.sequence); + expect(messageData.vaaTime).to.equal(parsed.timestamp); + expect(messageData.vaaVersion).to.equal(parsed.version); + + const signatureSetData = await getSignatureSetData( + connection, + messageData.vaaSignatureAccount + ); + const signed = signatureSetData.signatures; + expect(signed).has.length(guardians.signers.length); + expect(signed.filter((x) => !x)).has.length( + 19 - signingGuardians.length + ); + for (const i of signingGuardians) { + expect(signed[i]).is.true; + } + expect(Buffer.compare(signatureSetData.hash, parsed.hash)).to.equal(0); + expect(signatureSetData.guardianSetIndex).to.equal(guardians.setIndex); + }); + + it("Post VAA Signed with 19 Guardians", async () => { + const message = Buffer.from("All your base are belong to us."); + const nonce = 0; + const consistencyLevel = 15; + const published = ethereumWormhole.publishMessage( + nonce, + message, + consistencyLevel + ); + const signingGuardians = [...Array(19).keys()]; + const signedVaa = guardians.addSignatures(published, signingGuardians); + // console.log(`signedVaa: ${signedVaa.toString("base64")}`); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + // verify data + const parsed = parseVaa(signedVaa); + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parsed.hash + ).then((postedVaa) => postedVaa.message); + expect(messageData.consistencyLevel).to.equal(consistencyLevel); + expect(messageData.consistencyLevel).to.equal(parsed.consistencyLevel); + expect( + Buffer.compare(messageData.emitterAddress, parsed.emitterAddress) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(parsed.emitterChain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.nonce).to.equal(parsed.nonce); + expect(Buffer.compare(messageData.payload, message)).to.equal(0); + expect(Buffer.compare(messageData.payload, parsed.payload)).to.equal(0); + expect(messageData.sequence).to.equal(parsed.sequence); + expect(messageData.vaaTime).to.equal(parsed.timestamp); + expect(messageData.vaaVersion).to.equal(parsed.version); + + const signatureSetData = await getSignatureSetData( + connection, + messageData.vaaSignatureAccount + ); + const signed = signatureSetData.signatures; + expect(signed).has.length(guardians.signers.length); + expect(signed.filter((x) => !x)).has.length( + 19 - signingGuardians.length + ); + for (const i of signingGuardians) { + expect(signed[i]).is.true; + } + expect(Buffer.compare(signatureSetData.hash, parsed.hash)).to.equal(0); + expect(signatureSetData.guardianSetIndex).to.equal(guardians.setIndex); + }); + }); + }); +}); diff --git a/testing/solana-test-validator/sdk-tests/2_token_bridge.ts b/testing/solana-test-validator/sdk-tests/2_token_bridge.ts new file mode 100644 index 000000000..a50057190 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/2_token_bridge.ts @@ -0,0 +1,1982 @@ +import { expect } from "chai"; +import * as web3 from "@solana/web3.js"; +import { + createMint, + getAccount, + getAssociatedTokenAddressSync, + getMint, + getOrCreateAssociatedTokenAccount, + mintTo, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + MockGuardians, + GovernanceEmitter, + MockEthereumTokenBridge, +} from "../../../sdk/js/src/mock"; +import { + createApproveAuthoritySignerInstruction, + createAttestTokenInstruction, + createCompleteTransferNativeInstruction, + createCompleteTransferWrappedInstruction, + createCreateWrappedInstruction, + createRegisterChainInstruction, + createTransferNativeInstruction, + createTransferNativeWithPayloadInstruction, + createTransferWrappedInstruction, + createTransferWrappedWithPayloadInstruction, + deriveCustodyKey, + deriveEndpointKey, + deriveMintAuthorityKey, + deriveWrappedMintKey, + getAttestTokenAccounts, + getCompleteTransferNativeAccounts, + getCompleteTransferWrappedAccounts, + getCreateWrappedAccounts, + getEndpointRegistration, + getInitializeAccounts, + getRegisterChainAccounts, + getTransferNativeAccounts, + getTransferNativeWithPayloadAccounts, + getTransferWrappedAccounts, + getTransferWrappedWithPayloadAccounts, + getUpgradeContractAccounts, + getWrappedMeta, +} from "../../../sdk/js/src/solana/tokenBridge"; +import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa"; +import { + BpfLoaderUpgradeable, + getCompleteTransferNativeWithPayloadCpiAccounts, + getCompleteTransferWrappedWithPayloadCpiAccounts, + getTransferNativeWithPayloadCpiAccounts, + getTransferWrappedWithPayloadCpiAccounts, + NodeWallet, + SplTokenMetadataProgram, +} from "../../../sdk/js/src/solana"; +import { + deriveWormholeEmitterKey, + getPostedMessage, + getPostedVaa, +} from "../../../sdk/js/src/solana/wormhole"; +import { + parseGovernanceVaa, + parseAttestMetaVaa, + parseAttestMetaPayload, + parseTokenBridgeRegisterChainVaa, + parseTokenTransferPayload, + parseVaa, +} from "../../../sdk/js/src/vaa"; + +import { + CORE_BRIDGE_ADDRESS, + TOKEN_BRIDGE_ADDRESS, + ETHEREUM_TOKEN_BRIDGE_ADDRESS, + GOVERNANCE_EMITTER_ADDRESS, + GUARDIAN_KEYS, + GUARDIAN_SET_INDEX, + LOCALHOST, + WETH_ADDRESS, +} from "./helpers/consts"; +import { ethAddressToBuffer, now } from "./helpers/utils"; +import { + getForeignAssetSolana, + getIsWrappedAssetSolana, + getOriginalAssetSolana, +} from "../../../sdk/js/src/token_bridge"; +import { ChainId } from "../../../sdk/js/src"; + +describe("Token Bridge", () => { + const connection = new web3.Connection(LOCALHOST, "processed"); + + const wallet = new NodeWallet(web3.Keypair.generate()); + + // for signing wormhole messages + const guardians = new MockGuardians(GUARDIAN_SET_INDEX + 1, GUARDIAN_KEYS); + + const localVariables: any = {}; + + before("Airdrop SOL", async () => { + await connection + .requestAirdrop(wallet.key(), 1000 * web3.LAMPORTS_PER_SOL) + .then(async (signature) => connection.confirmTransaction(signature)); + }); + + before("Create Mint", async () => { + localVariables.mint = await createMint( + connection, + wallet.signer(), + wallet.key(), + null, + 9 + ); + + localVariables.mintAta = await getOrCreateAssociatedTokenAccount( + connection, + wallet.signer(), + localVariables.mint, + wallet.key() + ).then((account) => account.address); + + const mintToTx = await mintTo( + connection, + wallet.signer(), + localVariables.mint, + localVariables.mintAta, + wallet.key(), + 1000 * web3.LAMPORTS_PER_SOL + ); + }); + + before("Create Mint with Metadata", async () => { + // TODO + }); + + describe("Accounts", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex") + ); + + // token bridge on Ethereum + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + const payer = new web3.PublicKey( + "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" + ); + + it("Instruction 0: Initialize", () => { + const accounts = getInitializeAccounts(TOKEN_BRIDGE_ADDRESS, payer); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 1: Attest Token", () => { + const mint = NATIVE_MINT; + const message = web3.Keypair.generate(); + const accounts = getAttestTokenAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + mint, + message.publicKey + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "8xGY7Bx9cWocPYpKRe3sjCYTdm3YchFJFHmJC5FenW6B" + ); + expect(accounts.splMetadata.toString()).to.equal( + "6dM4TqWyWJsbx7obrdLcviBkTafD5E8av61zfU6jq57X" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 2: Complete Native", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const amountEncoded = 42069n; + + const fee = 0n; + const nonce = 420; + const timestamp = 23456789; + const message = ethereumTokenBridge.publishTransferTokens( + mint.toBuffer().toString("hex"), + 1, + amountEncoded, + 1, + mintAta.toBuffer().toString("hex"), + fee, + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferNativeAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.vaa.toString()).to.equal( + "GMBEenvtgYkQrHZNkx3kYbdYEUYN1GFRGBFN172bk3cN" + ); + expect(accounts.claim.toString()).to.equal( + "HzjTihvhEx7BbKnB2KHATNBwGFCEm2nnMG6c4Pwx6pPE" + ); + expect(accounts.endpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.to.equals(mintAta)).is.true; + expect(accounts.toFees.equals(mintAta)).is.true; + expect(accounts.custody.toString()).to.equal( + "2Aczo4H847TNDsPradsVXQUZFquJ37ZoHhRmJ2MAqtiM" + ); + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.custodySigner.toString()).to.equal( + "Eb8xqkMEZYeTnDse4BgWiHVByeUj3JDpgbuz98pWdgPE" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 3: Complete Wrapped", () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const amount = 4206942069n; + const recipientChain = 1; + const fee = 0n; + const nonce = 420; + const timestamp = 34567890; + const message = ethereumTokenBridge.publishTransferTokens( + tokenAddress.toString("hex"), + tokenChain, + amount, + recipientChain, + mintAta.toBuffer().toString("hex"), + fee, + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferWrappedAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.vaa.toString()).to.equal( + "7NQnr9aG4xQCp9AjnF37L3CrBbM6GJ2gN98JeFjn7nnf" + ); + expect(accounts.claim.toString()).to.equal( + "7Ae57QxvZMwCrknoDWpeaMTLbMP3LBeCJee6KaLEwxP6" + ); + expect(accounts.endpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.to.equals(mintAta)).is.true; + expect(accounts.toFees.equals(mintAta)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.mintAuthority.toString()).to.equal( + "J2mhpFfGCwHtUjmeQGhJSa2yk5h3egRoSd1AUhaKx2WG" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 4: Transfer Wrapped", () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const message = web3.Keypair.generate(); + const accounts = getTransferWrappedAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + payer, + tokenChain, + tokenAddress + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.fromOwner.equals(payer)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 5: Transfer Native", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, payer); + const message = web3.Keypair.generate(); + const accounts = getTransferNativeAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + mint + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.custody.toString()).to.equal( + "2Aczo4H847TNDsPradsVXQUZFquJ37ZoHhRmJ2MAqtiM" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.custodySigner.toString()).to.equal( + "Eb8xqkMEZYeTnDse4BgWiHVByeUj3JDpgbuz98pWdgPE" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 6: Register Chain", () => { + const timestamp = 45678901; + const message = governance.publishTokenBridgeRegisterChain( + timestamp, + 2, + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getRegisterChainAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + const parsed = parseGovernanceVaa(signedVaa); + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.endpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.vaa.toString()).to.equal( + "92pVby4LJPSyQxSSHLYv3EdpqWjH5bBoLGBJAeQkunf8" + ); + expect(accounts.claim.toString()).to.equal( + "J5LWxMcXo1xmdZq57VD4wrUgvw5taizQ9QEPHooHTwJv" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 7: Create Wrapped", () => { + const tokenAddress = WETH_ADDRESS; + const decimals = 18; + const symbol = "WETH"; + const name = "Wrapped ETH"; + const nonce = 420; + const message = ethereumTokenBridge.publishAttestMeta( + tokenAddress, + decimals, + symbol, + name, + nonce + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + const accounts = getCreateWrappedAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.endpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.vaa.toString()).to.equal( + "AaZqjqvg8QirKetuR799Smfw5gyAWobUooGsvxzr1aoX" + ); + expect(accounts.claim.toString()).to.equal( + "4dyk94hhqektDX9wUBCL1ZkyQC1Xn3QaTSAdJeZzbTcJ" + ); + expect(accounts.mint.toString()).to.equal( + "3tUXFuBNWzZZ8p2xNx5UoWCH664M2KHdDAWrdZAD1VQ3" + ); + expect(accounts.wrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.splMetadata.toString()).to.equal( + "46nJp6UehY8XpgNsSZFTamdXcwiSEQpRGvbBCt2KvVUf" + ); + expect(accounts.mintAuthority.toString()).to.equal( + "J2mhpFfGCwHtUjmeQGhJSa2yk5h3egRoSd1AUhaKx2WG" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect( + accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId) + ).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 8: Upgrade Contract", () => { + const timestamp = 56789012; + const chain = 1; + const implementation = new web3.PublicKey( + "2B5wMnErS8oKWV1wPTNQQhM1WLyxee2obtBMDtsYeHgA" + ); + const message = governance.publishTokenBridgeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getUpgradeContractAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.vaa.toString()).to.equal( + "HM2U2HEfbjrkvYLvry8Sqfmd5cCVxq6RjdLUUrNW2ELR" + ); + expect(accounts.claim.toString()).to.equal( + "9jDWWzAosaD6EWH9SMFT3ZwJnDZTeGcCdMU8H5Ba7dpx" + ); + expect(accounts.upgradeAuthority.toString()).to.equal( + "B2LFmpNCkfBFpoorLy4BghGbZyi5sdRsbjxSSASpjUoA" + ); + expect(accounts.spill.equals(payer)).is.true; + expect(accounts.implementation.equals(implementation)).is.true; + expect(accounts.programData.toString()).to.equal( + "3zHkdon6x9fUVqjxu6fCgdp3qMxLFZz59pj1H2NtnbGe" + ); + expect(accounts.tokenBridgeProgram.equals(TOKEN_BRIDGE_ADDRESS)).to.be + .true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect( + accounts.bpfLoaderUpgradeable.equals(BpfLoaderUpgradeable.programId) + ).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 11: Transfer Wrapped With Payload", () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const message = web3.Keypair.generate(); + const accounts = getTransferWrappedWithPayloadAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + payer, + tokenChain, + tokenAddress + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.fromOwner.equals(payer)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.sender.equals(payer)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 12: Transfer Native With Payload", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, payer); + const message = web3.Keypair.generate(); + const accounts = getTransferNativeWithPayloadAccounts( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + mint + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.custody.toString()).to.equal( + "2Aczo4H847TNDsPradsVXQUZFquJ37ZoHhRmJ2MAqtiM" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.custodySigner.toString()).to.equal( + "Eb8xqkMEZYeTnDse4BgWiHVByeUj3JDpgbuz98pWdgPE" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.sender.equals(payer)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + }); + + describe("CPI Accounts", () => { + // token bridge on Ethereum + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + const payer = new web3.PublicKey( + "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" + ); + + // mock program integrating token bridge + const cpiProgramId = new web3.PublicKey( + "pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ" + ); + + it("getCompleteTransferNativeWithPayloadCpiAccounts", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, cpiProgramId, true); + + const amountEncoded = 42069n; + + const nonce = 420; + const timestamp = 23456789; + const message = ethereumTokenBridge.publishTransferTokensWithPayload( + mint.toBuffer().toString("hex"), + 1, + amountEncoded, + 1, + mintAta.toBuffer().toString("hex"), + Buffer.alloc(32, 0), + Buffer.from("All your base are belong to us"), + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferNativeWithPayloadCpiAccounts( + cpiProgramId, + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.tokenBridgeConfig.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.vaa.toString()).to.equal( + "4qSDmrTmsUziHvKjjcCpLpfZNaRuxpKcuE95T6875jb1" + ); + expect(accounts.tokenBridgeClaim.toString()).to.equal( + "HzjTihvhEx7BbKnB2KHATNBwGFCEm2nnMG6c4Pwx6pPE" + ); + expect(accounts.tokenBridgeForeignEndpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.toTokenAccount.equals(mintAta)).is.true; + expect(accounts.tokenBridgeRedeemer.toString()).to.equal( + "A2SNTmahH9ryK2PupNMfKibPPaMtcfYBSX4WjZchhatX" + ); + expect(accounts.toFeesTokenAccount.equals(mintAta)).is.true; + expect(accounts.tokenBridgeCustody.toString()).to.equal( + "2Aczo4H847TNDsPradsVXQUZFquJ37ZoHhRmJ2MAqtiM" + ); + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.tokenBridgeCustodySigner.toString()).to.equal( + "Eb8xqkMEZYeTnDse4BgWiHVByeUj3JDpgbuz98pWdgPE" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("getCompleteTransferWrappedWithPayloadCpiAccounts", () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, cpiProgramId, true); + + const amount = 4206942069n; + const recipientChain = 1; + const nonce = 420; + const timestamp = 34567890; + const message = ethereumTokenBridge.publishTransferTokensWithPayload( + tokenAddress.toString("hex"), + tokenChain, + amount, + recipientChain, + mintAta.toBuffer().toString("hex"), + Buffer.alloc(32, 0), + Buffer.from("All your base are belong to us"), + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferWrappedWithPayloadCpiAccounts( + cpiProgramId, + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.tokenBridgeConfig.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.vaa.toString()).to.equal( + "A51Qrypowzrj4LgSRadTJ8p6ZnXH52oxFG6Su5u2zLPB" + ); + expect(accounts.tokenBridgeClaim.toString()).to.equal( + "7Ae57QxvZMwCrknoDWpeaMTLbMP3LBeCJee6KaLEwxP6" + ); + expect(accounts.tokenBridgeForeignEndpoint.toString()).to.equal( + "4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru" + ); + expect(accounts.toTokenAccount.equals(mintAta)).is.true; + expect(accounts.tokenBridgeRedeemer.toString()).to.equal( + "A2SNTmahH9ryK2PupNMfKibPPaMtcfYBSX4WjZchhatX" + ); + expect(accounts.toFeesTokenAccount.equals(mintAta)).is.true; + expect(accounts.tokenBridgeWrappedMint.equals(mint)).is.true; + expect(accounts.tokenBridgeWrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.tokenBridgeMintAuthority.toString()).to.equal( + "J2mhpFfGCwHtUjmeQGhJSa2yk5h3egRoSd1AUhaKx2WG" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("getTransferWrappedWithPayloadCpiAccounts", () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, cpiProgramId, true); + + const message = web3.Keypair.generate(); + const accounts = getTransferWrappedWithPayloadCpiAccounts( + cpiProgramId, + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + tokenChain, + tokenAddress + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.tokenBridgeConfig.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.fromTokenAccount.equals(mintAta)).is.true; + expect(accounts.fromTokenAccountOwner.equals(cpiProgramId)).is.true; + expect(accounts.tokenBridgeWrappedMint.equals(mint)).is.true; + expect(accounts.tokenBridgeWrappedMeta.toString()).to.equal( + "AWUK8RTEBvUNAWLz1VfagK3rnvJ9oLDZPBJEBCzpjqj7" + ); + expect(accounts.tokenBridgeAuthoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.tokenBridgeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.tokenBridgeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.tokenBridgeSender.toString()).to.equal( + "7r3GbMGbRRp3cbPRPv9v5GBktxGpDmK5LBnvjDVxsEDN" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("getTransferNativeWithPayloadCpiAccounts", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, cpiProgramId, true); + const message = web3.Keypair.generate(); + const accounts = getTransferNativeWithPayloadCpiAccounts( + cpiProgramId, + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + mint + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.tokenBridgeConfig.toString()).to.equal( + "GnQ6fGttTRnJpAJuy2XEg5TLgEMtbyU4HDJnBWmojsTv" + ); + expect(accounts.fromTokenAccount.equals(mintAta)).is.true; + expect(accounts.tokenBridgeCustody.toString()).to.equal( + "2Aczo4H847TNDsPradsVXQUZFquJ37ZoHhRmJ2MAqtiM" + ); + expect(accounts.tokenBridgeAuthoritySigner.toString()).to.equal( + "FDYbeBnX3rZnWM1jE6vTSxjxYdeGryxZtirXmzW71FTH" + ); + expect(accounts.tokenBridgeCustodySigner.toString()).to.equal( + "Eb8xqkMEZYeTnDse4BgWiHVByeUj3JDpgbuz98pWdgPE" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.tokenBridgeEmitter.toString()).to.equal( + "Ard2Zy4HckbJS2bL7y4361wbKSUH68JZqYBura5d4xtw" + ); + expect(accounts.tokenBridgeSequence.toString()).to.equal( + "Gdeob8iLpTN4Fc8BEgRdFUWikdUsvrv9Rfc1rNQWy4b7" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.tokenBridgeSender.toString()).to.equal( + "7r3GbMGbRRp3cbPRPv9v5GBktxGpDmK5LBnvjDVxsEDN" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + }); + + describe("Token Bridge Program Interaction", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), + 20 + ); + + // token bridge on Ethereum + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + describe("Setup Token Bridge", () => { + it("Register Ethereum Token Bridge", async () => { + const timestamp = now(); + const message = governance.publishTokenBridgeRegisterChain( + timestamp, + 2, + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const registerChainTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createRegisterChainInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ) + ), + [wallet.signer()] + ); + // console.log(`registerChainTx: ${registerChainTx}`); + + // verify data + const parsed = parseTokenBridgeRegisterChainVaa(signedVaa); + const endpoint = deriveEndpointKey( + TOKEN_BRIDGE_ADDRESS, + parsed.foreignChain, + parsed.foreignAddress + ); + const endpointRegistration = await getEndpointRegistration( + connection, + endpoint + ); + expect(endpointRegistration.chain).to.equal(2); + + const expectedEmitter = ethAddressToBuffer( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + expect( + Buffer.compare(endpointRegistration.contract, expectedEmitter) + ).to.equal(0); + }); + }); + + describe("Native Token Handling", () => { + it("Attest Mint Without Metadata", async () => { + const mint = localVariables.mint; + const message = web3.Keypair.generate(); + const nonce = 69; + + const attestTokenTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createAttestTokenInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + mint, + message.publicKey, + nonce + ) + ), + [wallet.signer(), message] + ); + // console.log(`attestTokenTx: ${attestTokenTx}`); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(TOKEN_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(0n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const assetMeta = parseAttestMetaPayload(messageData.payload); + expect(assetMeta.payloadType).to.equal(2); + expect( + Buffer.compare(assetMeta.tokenAddress, mint.toBuffer()) + ).to.equal(0); + expect(assetMeta.tokenChain).to.equal(1); + expect(assetMeta.decimals).to.equal(9); + expect(assetMeta.symbol).to.equal(""); + expect(assetMeta.name).to.equal(""); + }); + + // it("Attest Mint With Metadata", async () => { + // // TODO + // }); + + it("Send Token", async () => { + const mint = localVariables.mint; + const mintAta = localVariables.mintAta; + const custodyAccount = deriveCustodyKey(TOKEN_BRIDGE_ADDRESS, mint); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceBefore = 0n; + + const nonce = 69; + const amount = BigInt(420 * web3.LAMPORTS_PER_SOL); + const fee = 0n; + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + TOKEN_BRIDGE_ADDRESS, + mintAta, + wallet.key(), + amount + ); + + const message = web3.Keypair.generate(); + const transferNativeIx = createTransferNativeInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + mint, + nonce, + amount, + fee, + targetAddress, + targetChain + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferNativeIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceAfter = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(amount); + expect(custodyBalanceAfter - custodyBalanceBefore).to.equal(amount); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(TOKEN_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(1n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(1); + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).greaterThan(8); + // decimals will be 8 on Ethereum token bridge + const amountEncoded = + amount / BigInt(Math.pow(10, mintInfo.decimals - 8)); + expect(tokenTransfer.amount).to.equal(amountEncoded); + expect(tokenTransfer.fee).is.not.null; + expect(tokenTransfer.fee).to.equal(fee); + expect(tokenTransfer.fromAddress).is.null; + expect(Buffer.compare(tokenTransfer.to, targetAddress)).to.equal(0); + expect(tokenTransfer.toChain).to.equal(targetChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, mint.toBuffer()) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(1); + }); + + it("Receive Token", async () => { + const mint = localVariables.mint; + const mintAta = localVariables.mintAta; + const custodyAccount = deriveCustodyKey(TOKEN_BRIDGE_ADDRESS, mint); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceBefore = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + const amount = 420n * BigInt(web3.LAMPORTS_PER_SOL); + + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).greaterThan(8); + // decimals will be 8 on Ethereum token bridge + const amountEncoded = + amount / BigInt(Math.pow(10, mintInfo.decimals - 8)); + + const tokenChain = 1; + const recipientChain = 1; + const fee = 0n; + const nonce = 420; + const message = ethereumTokenBridge.publishTransferTokens( + mint.toBuffer().toString("hex"), + tokenChain, + amountEncoded, + recipientChain, + mintAta.toBuffer().toString("hex"), + fee, + nonce + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const completeNativeTransferIx = + createCompleteTransferNativeInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const completeNativeTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(completeNativeTransferIx), + [wallet.signer()] + ); + // console.log(`completeNativeTransferTx: ${completeNativeTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceAfter = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + // check balance changes + expect(walletBalanceAfter - walletBalanceBefore).to.equal(amount); + expect(custodyBalanceBefore - custodyBalanceAfter).to.equal(amount); + + // verify data + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parseVaa(signedVaa).hash + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal( + ethereumTokenBridge.consistencyLevel + ); + expect( + Buffer.compare( + messageData.emitterAddress, + ethAddressToBuffer(ETHEREUM_TOKEN_BRIDGE_ADDRESS) + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(1n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaVersion).to.equal(1); + expect( + Buffer.compare(parseVaa(signedVaa).payload, messageData.payload) + ).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(1); + expect(tokenTransfer.amount).to.equal(amountEncoded); + expect(tokenTransfer.fee).is.not.null; + expect(tokenTransfer.fee).to.equal(fee); + expect(tokenTransfer.fromAddress).is.null; + expect(Buffer.compare(tokenTransfer.to, mintAta.toBuffer())).to.equal( + 0 + ); + expect(tokenTransfer.toChain).to.equal(recipientChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, mint.toBuffer()) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(tokenChain); + }); + + it("Send Token With Payload", async () => { + const mint = localVariables.mint; + const mintAta = localVariables.mintAta; + const custodyAccount = deriveCustodyKey(TOKEN_BRIDGE_ADDRESS, mint); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceBefore = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + const nonce = 420; + const amount = BigInt(69 * web3.LAMPORTS_PER_SOL); + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + TOKEN_BRIDGE_ADDRESS, + mintAta, + wallet.key(), + amount + ); + + const message = web3.Keypair.generate(); + const transferPayload = Buffer.from("All your base are belong to us"); + const transferNativeIx = createTransferNativeWithPayloadInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + mint, + nonce, + amount, + targetAddress, + targetChain, + transferPayload + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferNativeIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceAfter = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(amount); + expect(custodyBalanceAfter - custodyBalanceBefore).to.equal(amount); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(TOKEN_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(2n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(3); + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).greaterThan(8); + // decimals will be 8 on Ethereum token bridge + const amountEncoded = + amount / BigInt(Math.pow(10, mintInfo.decimals - 8)); + expect(tokenTransfer.amount).to.equal(amountEncoded); + expect(tokenTransfer.fee).is.null; + expect(tokenTransfer.fromAddress).is.not.null; + expect( + new web3.PublicKey(tokenTransfer.fromAddress!).equals(wallet.key()) + ).is.true; + expect(Buffer.compare(tokenTransfer.to, targetAddress)).to.equal(0); + expect(tokenTransfer.toChain).to.equal(targetChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, mint.toBuffer()) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(1); + expect( + Buffer.compare(tokenTransfer.tokenTransferPayload, transferPayload) + ).to.equal(0); + }); + }); + + describe("Token Bridge Wrapped Token Handling", () => { + it("Create Wrapped with Metadata", async () => { + const tokenAddress = WETH_ADDRESS; + const decimals = 18; + const symbol = "WETH"; + const name = "Wrapped ETH"; + const nonce = 420; + const message = ethereumTokenBridge.publishAttestMeta( + tokenAddress, + decimals, + symbol, + name, + nonce + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const createWrappedIx = createCreateWrappedInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const createWrappedTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(createWrappedIx), + [wallet.signer()] + ); + // console.log(`createWrappedTx: ${createWrappedTx}`); + + // verify data + const parsed = parseAttestMetaVaa(signedVaa); + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parsed.hash + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal( + ethereumTokenBridge.consistencyLevel + ); + const expectedEmitter = ethAddressToBuffer( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + expect( + Buffer.compare(messageData.emitterAddress, expectedEmitter) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(2n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaVersion).to.equal(1); + expect(Buffer.compare(parsed.payload, messageData.payload)).to.equal(0); + + const assetMeta = parseAttestMetaPayload(messageData.payload); + expect(assetMeta.payloadType).to.equal(2); + const expectedTokenAddress = ethAddressToBuffer(tokenAddress); + expect( + Buffer.compare(assetMeta.tokenAddress, expectedTokenAddress) + ).to.equal(0); + expect(assetMeta.tokenChain).to.equal(ethereumTokenBridge.chain); + expect(assetMeta.decimals).to.equal(decimals); + expect(assetMeta.symbol).to.equal(symbol); + expect(assetMeta.name).to.equal(name); + + // check wrapped mint + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + assetMeta.tokenChain, + assetMeta.tokenAddress + ); + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).to.equal(8); + expect(mintInfo.mintAuthority).is.not.null; + expect( + mintInfo.mintAuthority?.equals( + deriveMintAuthorityKey(TOKEN_BRIDGE_ADDRESS) + ) + ).is.true; + expect(mintInfo.supply).to.equal(0n); + + // check wrapped meta + const wrappedMeta = await getWrappedMeta( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + expect(wrappedMeta.chain).to.equal(ethereumTokenBridge.chain); + expect( + Buffer.compare(wrappedMeta.tokenAddress, expectedTokenAddress) + ).to.equal(0); + expect(wrappedMeta.originalDecimals).to.equal(decimals); + }); + + it("Receive Token", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = await getOrCreateAssociatedTokenAccount( + connection, + wallet.signer(), + mint, + wallet.key() + ).then((account) => account.address); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyBefore = await getMint(connection, mint).then( + (info) => info.supply + ); + + const amount = 2n * 4206942069n; + const recipientChain = 1; + const fee = 0n; + const nonce = 420; + const message = ethereumTokenBridge.publishTransferTokens( + tokenAddress.toString("hex"), + tokenChain, + amount, + recipientChain, + mintAta.toBuffer().toString("hex"), + fee, + nonce + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const completeTransferWrappedIx = + createCompleteTransferWrappedInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const completeWrappedTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(completeTransferWrappedIx), + [wallet.signer()] + ); + // console.log(`completeWrappedTransferTx: ${completeWrappedTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyAfter = await getMint(connection, mint).then( + (info) => info.supply + ); + + // check balance and supply changes + expect(walletBalanceAfter - walletBalanceBefore).to.equal(amount); + expect(supplyAfter - supplyBefore).to.equal(amount); + + // verify data + const parsed = parseVaa(signedVaa); + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parsed.hash + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal( + ethereumTokenBridge.consistencyLevel + ); + expect( + Buffer.compare( + messageData.emitterAddress, + ethAddressToBuffer(ETHEREUM_TOKEN_BRIDGE_ADDRESS) + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(3n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaVersion).to.equal(1); + expect( + Buffer.compare(parseVaa(signedVaa).payload, messageData.payload) + ).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(1); + expect(tokenTransfer.amount).to.equal(amount); + expect(tokenTransfer.fee).is.not.null; + expect(tokenTransfer.fee).to.equal(fee); + expect(tokenTransfer.fromAddress).is.null; + expect(Buffer.compare(tokenTransfer.to, mintAta.toBuffer())).to.equal( + 0 + ); + expect(tokenTransfer.toChain).to.equal(recipientChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, tokenAddress) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(tokenChain); + }); + + it("Send Token", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, wallet.key()); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyBefore = await getMint(connection, mint).then( + (info) => info.supply + ); + + const nonce = 69; + const amount = 4206942069n; + const fee = 0n; + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + TOKEN_BRIDGE_ADDRESS, + mintAta, + wallet.key(), + amount + ); + + const message = web3.Keypair.generate(); + const transferNativeIx = createTransferWrappedInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + wallet.key(), + tokenChain, + tokenAddress, + nonce, + amount, + fee, + targetAddress, + targetChain + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferNativeIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyAfter = await getMint(connection, mint).then( + (info) => info.supply + ); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(amount); + expect(supplyBefore - supplyAfter).to.equal(amount); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(TOKEN_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(3n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(1); + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).to.equal(8); + expect(tokenTransfer.amount).to.equal(amount); + expect(tokenTransfer.fee).is.not.null; + expect(tokenTransfer.fee).to.equal(fee); + expect(tokenTransfer.fromAddress).is.null; + expect(Buffer.compare(tokenTransfer.to, targetAddress)).to.equal(0); + expect(tokenTransfer.toChain).to.equal(targetChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, tokenAddress) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(tokenChain); + }); + + it("Send Token With Payload", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + const mintAta = getAssociatedTokenAddressSync(mint, wallet.key()); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyBefore = await getMint(connection, mint).then( + (info) => info.supply + ); + + const nonce = 69; + const amount = 4206942069n; + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + TOKEN_BRIDGE_ADDRESS, + mintAta, + wallet.key(), + amount + ); + + const message = web3.Keypair.generate(); + const transferPayload = Buffer.from("All your base are belong to us"); + const transferNativeIx = createTransferWrappedWithPayloadInstruction( + TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + wallet.key(), + tokenChain, + tokenAddress, + nonce, + amount, + targetAddress, + targetChain, + transferPayload + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferNativeIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyAfter = await getMint(connection, mint).then( + (info) => info.supply + ); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(amount); + expect(supplyBefore - supplyAfter).to.equal(amount); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(TOKEN_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(4n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const tokenTransfer = parseTokenTransferPayload(messageData.payload); + expect(tokenTransfer.payloadType).to.equal(3); + const mintInfo = await getMint(connection, mint); + expect(mintInfo.decimals).to.equal(8); + expect(tokenTransfer.amount).to.equal(amount); + expect(Buffer.compare(tokenTransfer.to, targetAddress)).to.equal(0); + expect(tokenTransfer.toChain).to.equal(targetChain); + expect( + Buffer.compare(tokenTransfer.tokenAddress, tokenAddress) + ).to.equal(0); + expect(tokenTransfer.tokenChain).to.equal(tokenChain); + expect( + Buffer.compare(tokenTransfer.tokenTransferPayload, transferPayload) + ).to.equal(0); + }); + }); + }); + + describe("Asset Queries", () => { + // nft bridge on Ethereum + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + describe("getOriginalAssetSolana", () => { + it("Non-existent Token", async () => { + const mint = "wot m8?"; + const asset = await getOriginalAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).to.be.false; + expect(asset.chainId).to.equal(1); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), Buffer.alloc(32)) + ).to.equal(0); + }); + + it("Native Token", async () => { + const mint = localVariables.mint; + + const asset = await getOriginalAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).to.be.false; + expect(asset.chainId).to.equal(1); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), mint.toBuffer()) + ).to.equal(0); + }); + + it("Wrapped Token", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + + const asset = await getOriginalAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).is.true; + expect(asset.chainId).to.equal(tokenChain); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), tokenAddress) + ).to.equal(0); + }); + }); + + describe("getForeignAssetSolana", () => { + it("Wrapped Token", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + + const asset = await getForeignAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + tokenChain as ChainId, + tokenAddress + ); + + // verify results + expect(asset).to.equal("3tUXFuBNWzZZ8p2xNx5UoWCH664M2KHdDAWrdZAD1VQ3"); + }); + }); + + describe("getIsWrappedAsset", () => { + it("Non-existent Token", async () => { + const mint = null; + + const isWrapped = await getIsWrappedAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + // @ts-ignore: mint is null + mint + ); + + // verify results + expect(isWrapped).to.be.false; + }); + + it("Native Token", async () => { + const mint = localVariables.mint; + + const isWrapped = await getIsWrappedAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(isWrapped).to.be.false; + }); + + it("Wrapped Token", async () => { + const tokenAddress = ethAddressToBuffer(WETH_ADDRESS); + const tokenChain = ethereumTokenBridge.chain; + const mint = deriveWrappedMintKey( + TOKEN_BRIDGE_ADDRESS, + tokenChain, + tokenAddress + ); + + const isWrapped = await getIsWrappedAssetSolana( + connection, + TOKEN_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(isWrapped).is.true; + }); + }); + }); +}); diff --git a/testing/solana-test-validator/sdk-tests/3_nft_bridge.ts b/testing/solana-test-validator/sdk-tests/3_nft_bridge.ts new file mode 100644 index 000000000..b1de7a142 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/3_nft_bridge.ts @@ -0,0 +1,1290 @@ +import { expect } from "chai"; +import * as web3 from "@solana/web3.js"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createMint, + getAccount, + getAssociatedTokenAddressSync, + getMint, + getOrCreateAssociatedTokenAccount, + mintTo, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + MockGuardians, + GovernanceEmitter, + MockEthereumNftBridge, +} from "../../../sdk/js/src/mock"; +import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa"; +import { + BpfLoaderUpgradeable, + getMetadata, + NodeWallet, + SplTokenMetadataProgram, +} from "../../../sdk/js/src/solana"; +import { + deriveWormholeEmitterKey, + getPostedMessage, + getPostedVaa, +} from "../../../sdk/js/src/solana/wormhole"; +import { + parseNftBridgeRegisterChainVaa, + parseNftTransferPayload, + parseVaa, +} from "../../../sdk/js/src/vaa"; + +import { + CORE_BRIDGE_ADDRESS, + NFT_BRIDGE_ADDRESS, + ETHEREUM_NFT_BRIDGE_ADDRESS, + GOVERNANCE_EMITTER_ADDRESS, + GUARDIAN_KEYS, + GUARDIAN_SET_INDEX, + LOCALHOST, + WETH_ADDRESS, +} from "./helpers/consts"; +import { ethAddressToBuffer, makeErc721Token, now } from "./helpers/utils"; +import { + createApproveAuthoritySignerInstruction, + createCompleteTransferNativeInstruction, + createCompleteTransferWrappedInstruction, + createCompleteWrappedMetaInstruction, + createRegisterChainInstruction, + createTransferNativeInstruction, + createTransferWrappedInstruction, + deriveCustodyKey, + deriveEndpointKey, + deriveWrappedMintKey, + getCompleteTransferNativeAccounts, + getCompleteTransferWrappedAccounts, + getCompleteWrappedMetaAccounts, + getEndpointRegistration, + getInitializeAccounts, + getRegisterChainAccounts, + getTransferNativeAccounts, + getTransferWrappedAccounts, + getUpgradeContractAccounts, + getWrappedMeta, + mintToTokenId, + NFT_TRANSFER_NATIVE_TOKEN_ADDRESS, +} from "../../../sdk/js/src/solana/nftBridge"; +import { + getForeignAssetSolana, + getIsWrappedAssetSolana, + getOriginalAssetSolana, +} from "../../../sdk/js/src/nft_bridge"; +import { ChainId } from "../../../sdk/js/src"; + +describe("NFT Bridge", () => { + const connection = new web3.Connection(LOCALHOST, "processed"); + + const wallet = new NodeWallet(web3.Keypair.generate()); + + // for signing wormhole messages + const guardians = new MockGuardians(GUARDIAN_SET_INDEX + 1, GUARDIAN_KEYS); + + const erc721Token = makeErc721Token( + WETH_ADDRESS, + 6969n, + "Wetherean", + "WETH", + "https://ethereum.org/en/developers/tutorials/how-to-write-and-deploy-an-nft/" + ); + + const localVariables: any = {}; + + before("Airdrop SOL", async () => { + await connection + .requestAirdrop(wallet.key(), 1000 * web3.LAMPORTS_PER_SOL) + .then(async (signature) => connection.confirmTransaction(signature)); + }); + + before("Create NFT", async () => { + localVariables.mint = await createMint( + connection, + wallet.signer(), + wallet.key(), + null, + 0 + ); + + localVariables.nftMeta = { + name: "Space Cadet", + symbol: "CADET", + uri: "https://spl.solana.com/token#example-create-a-non-fungible-token", + }; + + const mint = localVariables.mint; + const name = localVariables.nftMeta.name; + const symbol = localVariables.nftMeta.symbol; + const updateAuthorityIsSigner = false; + const uri = localVariables.nftMeta.uri; + const creators = null; + const sellerFeeBasisPoints = 0; + const isMutable = false; + const createMetadataIx = SplTokenMetadataProgram.createMetadataAccounts( + wallet.key(), + mint, + wallet.key(), + name, + symbol, + wallet.key(), + updateAuthorityIsSigner, + uri, + creators, + sellerFeeBasisPoints, + isMutable + ); + + const createMetadataTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(createMetadataIx), + [wallet.signer()] + ); + // console.log("createMatadataTx", createMetadataTx); + + localVariables.mintAta = await getOrCreateAssociatedTokenAccount( + connection, + wallet.signer(), + localVariables.mint, + wallet.key() + ).then((account) => account.address); + + const mintToTx = await mintTo( + connection, + wallet.signer(), + localVariables.mint, + localVariables.mintAta, + wallet.key(), + 1 + ); + }); + + describe("Accounts", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex") + ); + + // nft bridge on Ethereum + const ethereumNftBridge = new MockEthereumNftBridge( + ETHEREUM_NFT_BRIDGE_ADDRESS + ); + + const payer = new web3.PublicKey( + "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" + ); + + it("Instruction 0: Initialize", () => { + const accounts = getInitializeAccounts(NFT_BRIDGE_ADDRESS, payer); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + + it("Instruction 1: Complete Native", () => { + const timestamp = 12345678; + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const nftMeta = localVariables.nftMeta; + const tokenId = mintToTokenId(mint); + const nonce = 420; + const message = ethereumNftBridge.publishTransferNft( + NFT_TRANSFER_NATIVE_TOKEN_ADDRESS.toString("hex"), + 1, + nftMeta.name, + nftMeta.symbol, + tokenId, + nftMeta.uri, + 1, + mintAta.toBuffer().toString("hex"), + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferNativeAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.vaa.toString()).to.equal( + "8b6y2t5NngJxhyicDkMQGWbrFagKEiiuGz2bqsVtf6Ks" + ); + expect(accounts.claim.toString()).to.equal( + "G3CvkGY9Pf7zMKmxxfN4UdaqDYcfQoKCoEZeCqh6ZQLR" + ); + expect(accounts.endpoint.toString()).to.equal( + "GGobvHkLNgwD7qnMRLFniLjoAtr12H4bqPD6AEHWzCou" + ); + expect(accounts.to.equals(mintAta)).is.true; + expect(accounts.toAuthority.equals(payer)).is.true; + expect(accounts.custody.toString()).to.equal( + "3ju9P66Ng9PEPhjY9HUDiC6taExZgWGULTERcP8RzT2j" + ); + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.custodySigner.toString()).to.equal( + "HHJPgZGoLrh8VmpmR4kPWmVoo8SZyAWQAVe15UtFJKQ1" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 2: Complete Wrapped", () => { + const timestamp = 23456789; + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const name = erc721Token.name; + const symbol = erc721Token.symbol; + const uri = erc721Token.uri; + + const recipientChain = 1; + const nonce = 420; + const message = ethereumNftBridge.publishTransferNft( + tokenAddress.toString("hex"), + tokenChain, + name, + symbol, + tokenId, + uri, + recipientChain, + mintAta.toBuffer().toString("hex"), + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteTransferWrappedAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.vaa.toString()).to.equal( + "2bNynrs1qu3tRah8ztXG1eY6XaiP1xPXSDxYim8DGVsQ" + ); + expect(accounts.claim.toString()).to.equal( + "5svagh2zqoHkBewBq2KRKQDJHYHwUzLJNt2sfgpRWLuj" + ); + expect(accounts.endpoint.toString()).to.equal( + "GGobvHkLNgwD7qnMRLFniLjoAtr12H4bqPD6AEHWzCou" + ); + expect(accounts.to.equals(mintAta)).is.true; + expect(accounts.toAuthority.equals(payer)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "GjxBVsD3fHPa3R97B4uA6TaMbuKXrQ9So8ktdNA6agXs" + ); + expect(accounts.mintAuthority.toString()).to.equal( + "ESeW7kNyP8mvfeqKkZdWkuFsKYeZnQUbrxYnNL8N11hi" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect( + accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId) + ).is.true; + expect( + accounts.associatedTokenProgram.equals(ASSOCIATED_TOKEN_PROGRAM_ID) + ).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 3: Complete Wrapped Meta", () => { + const timestamp = 34567890; + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const name = erc721Token.name; + const symbol = erc721Token.symbol; + const uri = erc721Token.uri; + + const recipientChain = 1; + const nonce = 420; + const message = ethereumNftBridge.publishTransferNft( + tokenAddress.toString("hex"), + tokenChain, + name, + symbol, + tokenId, + uri, + recipientChain, + mintAta.toBuffer().toString("hex"), + nonce, + timestamp + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getCompleteWrappedMetaAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.vaa.toString()).to.equal( + "4ZxueqFJAomm96wiDcxgedvyGTGVSp8HMxCz731xUGFd" + ); + expect(accounts.endpoint.toString()).to.equal( + "GGobvHkLNgwD7qnMRLFniLjoAtr12H4bqPD6AEHWzCou" + ); + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "GjxBVsD3fHPa3R97B4uA6TaMbuKXrQ9So8ktdNA6agXs" + ); + expect(accounts.splMetadata.toString()).to.equal( + "HgFdrXZJt1LzGuowL7KFP5JH7dgp2r22wRHijpgH4x9s" + ); + expect(accounts.mintAuthority.toString()).to.equal( + "ESeW7kNyP8mvfeqKkZdWkuFsKYeZnQUbrxYnNL8N11hi" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect( + accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId) + ).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 4: Transfer Wrapped", () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const message = web3.Keypair.generate(); + const accounts = getTransferWrappedAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + payer, + tokenChain, + tokenAddress, + tokenId + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.fromOwner.equals(payer)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.wrappedMeta.toString()).to.equal( + "GjxBVsD3fHPa3R97B4uA6TaMbuKXrQ9So8ktdNA6agXs" + ); + expect(accounts.splMetadata.toString()).to.equal( + "HgFdrXZJt1LzGuowL7KFP5JH7dgp2r22wRHijpgH4x9s" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "9xMX62GupB5AhucZyD3oC6aBd1NHsBCA6e1x9fez1zHe" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeMessage.equals(message.publicKey)).is.true; + expect(accounts.wormholeEmitter.toString()).to.equal( + "6rbfWsCH5vFbwKAkaAvBSP1Mom6ZkCaCk2pGAbxWt1CH" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "J1hwmV7YfVygE96jVFgWRE8F1niWRQ9pkQDr61551XpG" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect( + accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId) + ).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 5: Transfer Native", () => { + const mint = NATIVE_MINT; + const mintAta = getAssociatedTokenAddressSync(mint, payer); + + const message = web3.Keypair.generate(); + const accounts = getTransferNativeAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + message.publicKey, + mintAta, + mint + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.from.equals(mintAta)).is.true; + expect(accounts.mint.equals(mint)).is.true; + expect(accounts.splMetadata.toString()).to.equal( + "6dM4TqWyWJsbx7obrdLcviBkTafD5E8av61zfU6jq57X" + ); + expect(accounts.custody.toString()).to.equal( + "3ju9P66Ng9PEPhjY9HUDiC6taExZgWGULTERcP8RzT2j" + ); + expect(accounts.authoritySigner.toString()).to.equal( + "9xMX62GupB5AhucZyD3oC6aBd1NHsBCA6e1x9fez1zHe" + ); + expect(accounts.custodySigner.toString()).to.equal( + "HHJPgZGoLrh8VmpmR4kPWmVoo8SZyAWQAVe15UtFJKQ1" + ); + expect(accounts.wormholeBridge.toString()).to.equal( + "DNN2VhmrGTGj6QVnPz4NVfsiSk64cRHzKBLP5kUaQrf8" + ); + expect(accounts.wormholeMessage.equals(message.publicKey)).is.true; + expect(accounts.wormholeEmitter.toString()).to.equal( + "6rbfWsCH5vFbwKAkaAvBSP1Mom6ZkCaCk2pGAbxWt1CH" + ); + expect(accounts.wormholeSequence.toString()).to.equal( + "J1hwmV7YfVygE96jVFgWRE8F1niWRQ9pkQDr61551XpG" + ); + expect(accounts.wormholeFeeCollector.toString()).to.equal( + "Cxt3Uka7X8vyHYjU6szcuYVPPFyg1fAtoeVy7eyzPjGV" + ); + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true; + expect( + accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId) + ).is.true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 6: Register Chain", () => { + const timestamp = 45678901; + const message = governance.publishNftBridgeRegisterChain( + timestamp, + 2, + ETHEREUM_NFT_BRIDGE_ADDRESS + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const accounts = getRegisterChainAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.config.toString()).to.equal( + "J1oLBQPejgP75y9mKAAfftaQtmLhLkuQzbCufmYKMSQz" + ); + expect(accounts.endpoint.toString()).to.equal( + "GGobvHkLNgwD7qnMRLFniLjoAtr12H4bqPD6AEHWzCou" + ); + expect(accounts.vaa.toString()).to.equal( + "DU2VB93gzJ7Qb8xskdHXF4u3nFoAHD4L5DE1kLtXBHCJ" + ); + expect(accounts.claim.toString()).to.equal( + "6QuJAFuXYpz8WvzbMoS41mFki5mdLQswhkxoccg2tmTS" + ); + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true; + }); + + it("Instruction 7: Upgrade Contract", () => { + const timestamp = 56789012; + const chain = 1; + const implementation = new web3.PublicKey( + "2B5wMnErS8oKWV1wPTNQQhM1WLyxee2obtBMDtsYeHgA" + ); + const message = governance.publishNftBridgeUpgradeContract( + timestamp, + chain, + implementation.toString() + ); + const signedVaa = guardians.addSignatures(message, [0]); + + const accounts = getUpgradeContractAccounts( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + payer, + signedVaa + ); + + // verify accounts + expect(accounts.payer.equals(payer)).is.true; + expect(accounts.vaa.toString()).to.equal( + "Evar3arhnjy84wPDUpKPifxFRT9oRJFzwYZAVUKpsTnd" + ); + expect(accounts.claim.toString()).to.equal( + "3gHw5uPbhk1dDoCUxtK5VosaqTgV9H7wbNJsttswB4At" + ); + expect(accounts.upgradeAuthority.toString()).to.equal( + "8GUsAHTGAjJv5XgdQrHprwVnzqARbWaxZ3GHvquhZhpp" + ); + expect(accounts.spill.equals(payer)).is.true; + expect(accounts.implementation.equals(implementation)).is.true; + expect(accounts.programData.toString()).to.equal( + "2oC7qvaYxBLg1msKS8rPgEab5VSQ7TUFfhJBv75BzdSS" + ); + expect(accounts.nftBridgeProgram.equals(NFT_BRIDGE_ADDRESS)).is.true; + expect(accounts.rent.equals(web3.SYSVAR_RENT_PUBKEY)).is.true; + expect(accounts.clock.equals(web3.SYSVAR_CLOCK_PUBKEY)).is.true; + expect( + accounts.bpfLoaderUpgradeable.equals(BpfLoaderUpgradeable.programId) + ).is.true; + expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be + .true; + }); + }); + + describe("NFT Bridge Program Interaction", () => { + // for generating governance wormhole messages + const governance = new GovernanceEmitter( + GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), + 30 + ); + + // nft bridge on Ethereum + const ethereumNftBridge = new MockEthereumNftBridge( + ETHEREUM_NFT_BRIDGE_ADDRESS + ); + + describe("Setup NFT Bridge", () => { + it("Register Ethereum NFT Bridge", async () => { + const timestamp = now(); + const message = governance.publishNftBridgeRegisterChain( + timestamp, + 2, + ETHEREUM_NFT_BRIDGE_ADDRESS + ); + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const registerChainTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createRegisterChainInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ) + ), + [wallet.signer()] + ); + // console.log(`registerChainTx: ${registerChainTx}`); + + // verify data + const parsed = parseNftBridgeRegisterChainVaa(signedVaa); + const endpoint = deriveEndpointKey( + NFT_BRIDGE_ADDRESS, + parsed.foreignChain, + parsed.foreignAddress + ); + const endpointRegistration = await getEndpointRegistration( + connection, + endpoint + ); + expect(endpointRegistration.chain).to.equal(2); + const expectedEmitter = ethAddressToBuffer(ETHEREUM_NFT_BRIDGE_ADDRESS); + expect( + Buffer.compare(endpointRegistration.contract, expectedEmitter) + ).to.equal(0); + }); + }); + + describe("Native Token Handling", () => { + it("Send NFT", async () => { + const mint: web3.PublicKey = localVariables.mint; + const mintAta: web3.PublicKey = localVariables.mintAta; + const custodyAccount = deriveCustodyKey(NFT_BRIDGE_ADDRESS, mint); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceBefore = 0n; + + const nonce = 69; + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + NFT_BRIDGE_ADDRESS, + mintAta, + wallet.key() + ); + + const message = web3.Keypair.generate(); + const transferNativeIx = createTransferNativeInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + mint, + nonce, + targetAddress, + targetChain + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferNativeIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceAfter = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(1n); + expect(custodyBalanceAfter - custodyBalanceBefore).to.equal(1n); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(NFT_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(0n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const nftTransfer = parseNftTransferPayload(messageData.payload); + const nftMeta = localVariables.nftMeta; + expect(nftTransfer.payloadType).to.equal(1); + expect( + Buffer.compare( + nftTransfer.tokenAddress, + NFT_TRANSFER_NATIVE_TOKEN_ADDRESS + ) + ).to.equal(0); + expect(nftTransfer.tokenChain).to.equal(1); + expect(nftTransfer.name).to.equal(nftMeta.name); + expect(nftTransfer.symbol).to.equal(nftMeta.symbol); + expect(nftTransfer.tokenId).to.equal(mintToTokenId(mint)); + const expectedUri = Buffer.alloc(200); + expectedUri.write(nftMeta.uri, 0); + expect(nftTransfer.uri).to.equal(expectedUri.toString()); + expect(Buffer.compare(nftTransfer.to, targetAddress)).to.equal(0); + expect(nftTransfer.toChain).to.equal(targetChain); + }); + + it("Receive NFT", async () => { + const mint: web3.PublicKey = localVariables.mint; + const mintAta: web3.PublicKey = localVariables.mintAta; + const custodyAccount = deriveCustodyKey(NFT_BRIDGE_ADDRESS, mint); + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceBefore = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + const metadata = await getMetadata(connection, mint).then( + (info) => info.data + ); + + const tokenChain = 1; + const tokenId = mintToTokenId(mint); + const recipientChain = 1; + const nonce = 420; + const message = ethereumNftBridge.publishTransferNft( + NFT_TRANSFER_NATIVE_TOKEN_ADDRESS.toString("hex"), + tokenChain, + metadata.name, + metadata.symbol, + tokenId, + metadata.uri, + recipientChain, + mintAta.toBuffer().toString("hex"), + nonce + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const completeNativeTransferIx = + createCompleteTransferNativeInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const completeNativeTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(completeNativeTransferIx), + [wallet.signer()] + ); + // console.log(`completeNativeTransferTx: ${completeNativeTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const custodyBalanceAfter = await getAccount( + connection, + custodyAccount + ).then((account) => account.amount); + + // check balance changes + expect(walletBalanceAfter - walletBalanceBefore).to.equal(1n); + expect(custodyBalanceBefore - custodyBalanceAfter).to.equal(1n); + + // verify data + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parseVaa(signedVaa).hash + ).then((posted) => posted.message); + expect(messageData.consistencyLevel).to.equal( + ethereumNftBridge.consistencyLevel + ); + expect( + Buffer.compare( + messageData.emitterAddress, + ethAddressToBuffer(ETHEREUM_NFT_BRIDGE_ADDRESS) + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(ethereumNftBridge.chain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(1n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaVersion).to.equal(1); + expect( + Buffer.compare(parseVaa(signedVaa).payload, messageData.payload) + ).to.equal(0); + + const nftTransfer = parseNftTransferPayload(messageData.payload); + const nftMeta = localVariables.nftMeta; + expect(nftTransfer.payloadType).to.equal(1); + expect( + Buffer.compare( + nftTransfer.tokenAddress, + NFT_TRANSFER_NATIVE_TOKEN_ADDRESS + ) + ).to.equal(0); + expect(nftTransfer.tokenChain).to.equal(tokenChain); + expect(nftTransfer.name).to.equal(nftMeta.name); + expect(nftTransfer.symbol).to.equal(nftMeta.symbol); + expect(nftTransfer.tokenId).to.equal(mintToTokenId(mint)); + const expectedUri = Buffer.alloc(200); + expectedUri.write(nftMeta.uri, 0); + expect(nftTransfer.uri).to.equal(expectedUri.toString()); + expect(Buffer.compare(nftTransfer.to, mintAta.toBuffer())).to.equal(0); + expect(nftTransfer.toChain).to.equal(recipientChain); + }); + }); + + describe("NFT Bridge Wrapped Token Handling", () => { + it("Receive NFT and Create Metadata", async () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + const mintAta = getAssociatedTokenAddressSync(mint, wallet.key()); + + const name = erc721Token.name; + const symbol = erc721Token.symbol; + const uri = erc721Token.uri; + + // token account and mint don't exist yet, so there is no balance + const walletBalanceBefore = 0n; + const supplyBefore = 0n; + + const recipientChain = 1; + const nonce = 420; + const message = ethereumNftBridge.publishTransferNft( + tokenAddress.toString("hex"), + tokenChain, + name, + symbol, + tokenId, + uri, + recipientChain, + mintAta.toBuffer().toString("hex"), + nonce + ); + + const signedVaa = guardians.addSignatures( + message, + [0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18] + ); + + const txSignatures = await postVaa( + connection, + wallet.signTransaction, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ).then((results) => results.map((result) => result.signature)); + const postTx = txSignatures.pop()!; + for (const verifyTx of txSignatures) { + // console.log(`verifySignatures: ${verifyTx}`); + } + // console.log(`postVaa: ${postTx}`); + + const completeWrappedTransferIx = + createCompleteTransferWrappedInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const completeWrappedTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(completeWrappedTransferIx), + [wallet.signer()] + ); + // console.log(`completeWrappedTransferTx: ${completeWrappedTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyAfter = await getMint(connection, mint).then( + (info) => info.supply + ); + + // check balance changes + expect(walletBalanceAfter - walletBalanceBefore).to.equal(1n); + expect(supplyAfter - supplyBefore).to.equal(1n); + + // we need a separate transaction to execute complete_wrapped_meta instruction + // following complete_wrapped because... ??? + const completeWrappedMetaIx = createCompleteWrappedMetaInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + signedVaa + ); + + const completeWrappedMetaTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(completeWrappedMetaIx), + [wallet.signer()] + ); + // console.log(`completeWrappedMetaTx: ${completeWrappedMetaTx}`); + + // verify data + const messageData = await getPostedVaa( + connection, + CORE_BRIDGE_ADDRESS, + parseVaa(signedVaa).hash + ).then((posted) => posted.message); + expect(messageData.consistencyLevel).to.equal( + ethereumNftBridge.consistencyLevel + ); + expect( + Buffer.compare( + messageData.emitterAddress, + ethAddressToBuffer(ETHEREUM_NFT_BRIDGE_ADDRESS) + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(ethereumNftBridge.chain); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(2n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaVersion).to.equal(1); + expect( + Buffer.compare(parseVaa(signedVaa).payload, messageData.payload) + ).to.equal(0); + + const nftTransfer = parseNftTransferPayload(messageData.payload); + expect(nftTransfer.payloadType).to.equal(1); + expect(Buffer.compare(nftTransfer.tokenAddress, tokenAddress)).to.equal( + 0 + ); + expect(nftTransfer.tokenChain).to.equal(tokenChain); + expect(nftTransfer.name).to.equal(name); + expect(nftTransfer.symbol).to.equal(symbol); + expect(nftTransfer.tokenId).to.equal(tokenId); + expect(nftTransfer.uri).to.equal(uri); + expect(Buffer.compare(nftTransfer.to, mintAta.toBuffer())).to.equal(0); + expect(nftTransfer.toChain).to.equal(recipientChain); + + // check wrapped meta + const wrappedMeta = await getWrappedMeta( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + expect(wrappedMeta.chain).to.equal(tokenChain); + expect(Buffer.compare(wrappedMeta.tokenAddress, tokenAddress)).to.equal( + 0 + ); + expect(wrappedMeta.tokenId).to.equal(tokenId); + }); + + it("Send NFT", async () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + const mintAta = getAssociatedTokenAddressSync(mint, wallet.key()); + + const walletBalanceBefore = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyBefore = await getMint(connection, mint).then( + (info) => info.supply + ); + + const nonce = 69; + const targetAddress = Buffer.alloc(32, "deadbeef", "hex"); + const targetChain = 2; + + const approveIx = createApproveAuthoritySignerInstruction( + NFT_BRIDGE_ADDRESS, + mintAta, + wallet.key() + ); + + const message = web3.Keypair.generate(); + const transferWrappedIx = createTransferWrappedInstruction( + NFT_BRIDGE_ADDRESS, + CORE_BRIDGE_ADDRESS, + wallet.key(), + message.publicKey, + mintAta, + wallet.key(), + tokenChain, + tokenAddress, + tokenId, + nonce, + targetAddress, + targetChain + ); + + const approveAndTransferTx = await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add(approveIx, transferWrappedIx), + [wallet.signer(), message] + ); + // console.log(`approveAndTransferTx: ${approveAndTransferTx}`); + + const walletBalanceAfter = await getAccount(connection, mintAta).then( + (account) => account.amount + ); + const supplyAfter = await getMint(connection, mint).then( + (info) => info.supply + ); + + // check balance changes + expect(walletBalanceBefore - walletBalanceAfter).to.equal(1n); + expect(supplyBefore - supplyAfter).to.equal(1n); + + // verify data + const messageData = await getPostedMessage( + connection, + message.publicKey + ).then((posted) => posted.message); + expect(messageData.consistencyLevel).to.equal(32); + expect( + Buffer.compare( + messageData.emitterAddress, + deriveWormholeEmitterKey(NFT_BRIDGE_ADDRESS).toBuffer() + ) + ).to.equal(0); + expect(messageData.emitterChain).to.equal(1); + expect(messageData.nonce).to.equal(nonce); + expect(messageData.sequence).to.equal(1n); + expect(messageData.vaaTime).to.equal(0); + expect(messageData.vaaSignatureAccount.equals(web3.PublicKey.default)) + .is.true; + expect(messageData.vaaVersion).to.equal(0); + + const nftTransfer = parseNftTransferPayload(messageData.payload); + expect(nftTransfer.payloadType).to.equal(1); + expect(Buffer.compare(nftTransfer.tokenAddress, tokenAddress)).to.equal( + 0 + ); + expect(nftTransfer.tokenChain).to.equal(tokenChain); + expect(nftTransfer.name).to.equal(erc721Token.name); + expect(nftTransfer.symbol).to.equal(erc721Token.symbol); + expect(nftTransfer.tokenId).to.equal(tokenId); + // bridge does this cool thing of adding padding to the uri when being + // transferred out when it's wrapped + const expectedUri = Buffer.alloc(200); + expectedUri.write(erc721Token.uri, 0); + expect(nftTransfer.uri).to.equal(expectedUri.toString()); + expect(Buffer.compare(nftTransfer.to, targetAddress)).to.equal(0); + expect(nftTransfer.toChain).to.equal(targetChain); + }); + }); + }); + + describe("Asset Queries", () => { + // nft bridge on Ethereum + const ethereumNftBridge = new MockEthereumNftBridge( + ETHEREUM_NFT_BRIDGE_ADDRESS + ); + + describe("getOriginalAssetSolana", () => { + it("Non-existent NFT", async () => { + const mint = "wot m8?"; + const asset = await getOriginalAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).to.be.false; + expect(asset.chainId).to.equal(1); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), Buffer.alloc(32)) + ).to.equal(0); + expect(asset.tokenId).is.undefined; + }); + + it("Native NFT", async () => { + const mint = localVariables.mint; + + const asset = await getOriginalAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).to.be.false; + expect(asset.chainId).to.equal(1); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), mint.toBuffer()) + ).to.equal(0); + expect(asset.tokenId).is.undefined; + }); + + it("Wrapped NFT", async () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + + const asset = await getOriginalAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(asset.isWrapped).is.true; + expect(asset.chainId).to.equal(tokenChain); + expect( + Buffer.compare(Buffer.from(asset.assetAddress), tokenAddress) + ).to.equal(0); + expect(asset.tokenId).to.equal(tokenId.toString()); + }); + }); + + describe("getForeignAssetSolana", () => { + it("Wrapped NFT", async () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + + const asset = await getForeignAssetSolana( + NFT_BRIDGE_ADDRESS, + tokenChain as ChainId, + tokenAddress, + tokenId + ); + + // verify results + expect(asset).to.equal("GrWwR7tTfJvCLuNbcHAyhqBhuuCr8kG7xg47x47GshF4"); + }); + }); + + describe("getIsWrappedAsset", () => { + it("Non-existent NFT", async () => { + const mint = null; + + const isWrapped = await getIsWrappedAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + // @ts-ignore + mint + ); + + // verify results + expect(isWrapped).to.be.false; + }); + + it("Native NFT", async () => { + const mint = localVariables.mint; + + const isWrapped = await getIsWrappedAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(isWrapped).to.be.false; + }); + + it("Wrapped NFT", async () => { + const tokenAddress = ethAddressToBuffer(erc721Token.address); + const tokenChain = ethereumNftBridge.chain; + const tokenId = erc721Token.tokenId; + const mint = deriveWrappedMintKey( + NFT_BRIDGE_ADDRESS, + tokenChain, + tokenAddress, + tokenId + ); + + const isWrapped = await getIsWrappedAssetSolana( + connection, + NFT_BRIDGE_ADDRESS, + mint + ); + + // verify results + expect(isWrapped).is.true; + }); + }); + }); +}); diff --git a/testing/solana-test-validator/sdk-tests/helpers/consts.ts b/testing/solana-test-validator/sdk-tests/helpers/consts.ts new file mode 100644 index 000000000..7c02d92cc --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/helpers/consts.ts @@ -0,0 +1,52 @@ +import { PublicKey } from "@solana/web3.js"; + +export const LOCALHOST = "http://localhost:8899"; + +export const CORE_BRIDGE_ADDRESS = new PublicKey( + // "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" + "agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC" +); +export const TOKEN_BRIDGE_ADDRESS = new PublicKey( + // "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE" + "bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa" +); +export const NFT_BRIDGE_ADDRESS = new PublicKey( + // "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" + "caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q" +); + +export const GUARDIAN_SET_INDEX = 0; +export const GUARDIAN_KEYS = [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", + "c3b2e45c422a1602333a64078aeb42637370b0f48fe385f9cfa6ad54a8e0c47e", + "9f790d3f08bc4b5cd910d4278f3deb406e57bb5e924906ccd52052bb078ccd47", + "b20cc49d6f2c82a5e6519015fc18aa3e562867f85f872c58f1277cfbd2a0c8e4", + "eded5a2fdcb5bbbfa5b07f2a91393813420e7ac30a72fc935b6df36f8294b855", + "00d39587c3556f289677a837c7f3c0817cb7541ce6e38a243a4bdc761d534c5e", + "da534d61a8da77b232f3a2cee55c0125e2b3e33a5cd8247f3fe9e72379445c3b", + "cdbabfc2118eb00bc62c88845f3bbd03cb67a9e18a055101588ca9b36387006c", + "c83d36423820e7350428dc4abe645cb2904459b7d7128adefe16472fdac397ba", + "1cbf4e1388b81c9020500fefc83a7a81f707091bb899074db1bfce4537428112", + "17646a6ba14a541957fc7112cc973c0b3f04fce59484a92c09bb45a0b57eb740", + "eb94ff04accbfc8195d44b45e7c7da4c6993b2fbbfc4ef166a7675a905df9891", + "053a6527124b309d914a47f5257a995e9b0ad17f14659f90ed42af5e6e262b6a", + "3fbf1e46f6da69e62aed5670f279e818889aa7d8f1beb7fd730770fd4f8ea3d7", + "53b05697596ba04067e40be8100c9194cbae59c90e7870997de57337497172e9", + "4e95cb2ff3f7d5e963631ad85c28b1b79cb370f21c67cbdd4c2ffb0bf664aa06", + "01b8c448ce2c1d43cfc5938d3a57086f88e3dc43bb8b08028ecb7a7924f4676f", + "1db31a6ba3bcd54d2e8a64f8a2415064265d291593450c6eb7e9a6a986bd9400", + "70d8f1c9534a0ab61a020366b831a494057a289441c07be67e4288c44bc6cd5d", +]; + +export const GOVERNANCE_CHAIN = 1; +export const GOVERNANCE_EMITTER_ADDRESS = new PublicKey( + "11111111111111111111111111111115" +); + +export const ETHEREUM_WALLET_BYTES32 = + "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16"; +export const ETHEREUM_TOKEN_BRIDGE_ADDRESS = + "0x0290FB167208Af455bB137780163b7B7a9a10C16"; +export const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E"; +export const ETHEREUM_NFT_BRIDGE_ADDRESS = + "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"; diff --git a/testing/solana-test-validator/sdk-tests/helpers/utils.ts b/testing/solana-test-validator/sdk-tests/helpers/utils.ts new file mode 100644 index 000000000..c95781334 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/helpers/utils.ts @@ -0,0 +1,77 @@ +import { PublicKey } from "@solana/web3.js"; + +// used to use solana cli +const { execSync } = require("child_process"); + +export function now() { + return Math.floor(Date.now() / 1000); +} + +export function ethAddressToBuffer(address: string) { + return Buffer.concat([ + Buffer.alloc(12), + Buffer.from(address.substring(2), "hex"), + ]); +} + +interface Erc721Token { + address: string; + tokenId: bigint; + name: string; + symbol: string; + uri: string; +} + +export function makeErc721Token( + address: string, + tokenId: bigint, + name: string, + symbol: string, + uri: string +): Erc721Token { + return { + address, + tokenId, + name, + symbol, + uri, + }; +} + +export function deployProgram( + keyPath: string, + artifactPath: string, + programIdPath: string, + programId: PublicKey, // could derive it from programIdPath, but whatevs + upgradeAuthority: PublicKey +) { + // deploy + execSync( + `solana -k ${keyPath} program deploy ${artifactPath} --program-id ${programIdPath}` + ); + + // set upgrade authority + execSync( + `solana -k ${keyPath} program set-upgrade-authority ${programId.toString()} --new-upgrade-authority ${upgradeAuthority.toString()}` + ); +} + +export function execSolanaWriteBufferAndSetBufferAuthority( + keyPath: string, + artifactPath: string, + upgradeAuthority: PublicKey +): PublicKey { + // solana program write-buffer + const buffer = (() => { + const output = execSync( + `solana -k ${keyPath} program write-buffer ${artifactPath} -u localhost` + ); + return new PublicKey(output.toString().match(/^.{8}([A-Za-z0-9]+)/)[1]); + })(); + + // solana program set-buffer-authority + execSync( + `solana -k ${keyPath} program set-buffer-authority ${buffer.toString()} --new-buffer-authority ${upgradeAuthority.toString()} -u localhost` + ); + return buffer; +} diff --git a/testing/solana-test-validator/sdk-tests/keys/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC.json b/testing/solana-test-validator/sdk-tests/keys/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC.json new file mode 100644 index 000000000..51568bfc3 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/keys/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC.json @@ -0,0 +1 @@ +[128,165,242,120,7,6,102,188,105,243,13,108,82,70,58,65,156,132,87,62,237,134,93,236,187,139,89,47,158,2,250,155,8,161,40,36,141,192,245,141,26,199,217,79,109,84,183,17,244,193,247,58,94,24,189,159,117,161,21,222,140,88,168,7] \ No newline at end of file diff --git a/testing/solana-test-validator/sdk-tests/keys/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa.json b/testing/solana-test-validator/sdk-tests/keys/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa.json new file mode 100644 index 000000000..f43437531 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/keys/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa.json @@ -0,0 +1 @@ +[174,146,29,171,86,165,249,22,40,226,103,4,139,9,185,210,76,88,78,198,243,96,146,171,56,44,106,65,236,63,229,14,8,207,15,39,19,22,177,194,63,43,227,220,245,186,60,32,11,19,65,141,121,231,226,20,82,67,187,121,38,153,159,183] \ No newline at end of file diff --git a/testing/solana-test-validator/sdk-tests/keys/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q.json b/testing/solana-test-validator/sdk-tests/keys/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q.json new file mode 100644 index 000000000..72e548f5a --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/keys/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q.json @@ -0,0 +1 @@ +[111,86,175,36,46,209,140,201,215,246,14,51,66,248,60,179,87,253,25,85,154,229,3,143,196,168,24,105,74,231,149,200,9,29,142,44,208,57,127,100,73,118,189,247,247,43,122,16,8,162,175,27,232,196,154,83,186,252,166,119,215,115,70,175] \ No newline at end of file diff --git a/testing/solana-test-validator/sdk-tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json b/testing/solana-test-validator/sdk-tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json new file mode 100644 index 000000000..3e28fb443 --- /dev/null +++ b/testing/solana-test-validator/sdk-tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json @@ -0,0 +1 @@ +[112,55,233,99,229,91,68,85,207,63,10,46,103,0,49,250,22,189,30,167,157,146,26,148,175,155,212,104,86,182,185,192,12,26,88,134,254,16,147,223,159,196,56,194,150,249,247,39,91,119,24,182,188,14,21,109,141,51,108,88,240,131,153,109] \ No newline at end of file diff --git a/testing/solana-test-validator/tsconfig.json b/testing/solana-test-validator/tsconfig.json new file mode 100644 index 000000000..e1e56f611 --- /dev/null +++ b/testing/solana-test-validator/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2020"], + "module": "CommonJS", + "target": "es2020", + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "moduleResolution": "node" + } +} diff --git a/testing/solana-test-validator/yarn.lock b/testing/solana-test-validator/yarn.lock new file mode 100644 index 000000000..00c527f82 --- /dev/null +++ b/testing/solana-test-validator/yarn.lock @@ -0,0 +1,1504 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz" + integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@ethersproject/bytes@^5.6.1": + version "5.6.1" + resolved "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz" + integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== + dependencies: + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/logger@^5.6.0": + version "5.6.0" + resolved "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.6.0.tgz" + integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== + +"@ethersproject/sha2@^5.5.0": + version "5.6.1" + resolved "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.6.1.tgz" + integrity sha512-5K2GyqcW7G4Yo3uenHegbXRPDgARpWUiXc6RiF7b6i/HXUoWlb7uCARh7BAHg7/qT/Q5ydofNwiZcim9qpjB6g== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + hash.js "1.1.7" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@noble/ed25519@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.0.tgz#583ac38340a479314b9e348d4572101ed9492f9d" + integrity sha512-LeAxFK0+181zQOhOUuKE8Jnd3duzYhDNd3iCLxpmzA5K+e4I1FdbrK3Ot0ZHBwZMeRD/6EojyUfTbpHZ+hkQHg== + +"@noble/hashes@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" + integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== + +"@noble/secp256k1@^1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" + integrity sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ== + +"@project-serum/anchor@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.25.0.tgz#88ee4843336005cf5a64c80636ce626f0996f503" + integrity sha512-E6A5Y/ijqpfMJ5psJvbw0kVTzLZFUcOFgs6eSM2M2iWE1lVRF18T6hWZVNl6zqZsoz98jgnNHtVGJMs+ds9A7A== + dependencies: + "@project-serum/borsh" "^0.2.5" + "@solana/web3.js" "^1.36.0" + base64-js "^1.5.1" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^5.3.1" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + js-sha256 "^0.9.0" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@project-serum/borsh@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.5.tgz#6059287aa624ecebbfc0edd35e4c28ff987d8663" + integrity sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + +"@solana/buffer-layout@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz" + integrity sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ== + dependencies: + buffer "~6.0.3" + +"@solana/spl-token@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.1.tgz#20ba93f8e86b0913e6bfa49fc0708f7fd3bdaf5e" + integrity sha512-26/0XlW5Lyeu3CUlBGt+0o3l4H6AJtRtMMtsxhcKj+DwfGg+QMnPl/exTmZLEsymsn03PFhogd97v5fJXhYeow== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/web3.js" "^1.41.0" + +"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.41.0", "@solana/web3.js@^1.53.0": + version "1.53.0" + resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.53.0.tgz" + integrity sha512-QyQDA9U5b+AiTo1ANsj9WihWWECeLv6VRpiTE7xPe5hLYANXZYecnlLglNiEzVgRg/jLvR5DrCISXhHx/mAEJw== + dependencies: + "@babel/runtime" "^7.12.5" + "@ethersproject/sha2" "^5.5.0" + "@solana/buffer-layout" "^4.0.0" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.1" + fast-stable-stringify "^1.0.0" + jayson "^3.4.4" + js-sha3 "^0.8.0" + node-fetch "2" + react-native-url-polyfill "^1.3.0" + rpc-websockets "^7.5.0" + secp256k1 "^4.0.2" + superstruct "^0.14.2" + tweetnacl "^1.0.3" + +"@solana/web3.js@^1.36.0": + version "1.54.1" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.54.1.tgz#c39ffa598beaa6d761ab55c78263d874f4441e14" + integrity sha512-/PViPDGxF6oZZidcILndlm0MdbuzBouiQcqxrAfiBZ4lHMntLE4U75KhC+205EkVnkgCC4/prkjKVeSnbkfzrw== + dependencies: + "@babel/runtime" "^7.12.5" + "@noble/ed25519" "^1.7.0" + "@noble/hashes" "^1.1.2" + "@noble/secp256k1" "^1.6.3" + "@solana/buffer-layout" "^4.0.0" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.1" + fast-stable-stringify "^1.0.0" + jayson "^3.4.4" + js-sha3 "^0.8.0" + node-fetch "2" + rpc-websockets "^7.5.0" + superstruct "^0.14.2" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/bn.js@^5.1.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz" + integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== + dependencies: + "@types/node" "*" + +"@types/chai@^4.3.0": + version "4.3.1" + resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz" + integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ== + +"@types/connect@^3.4.33": + version "3.4.35" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.9": + version "4.17.29" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/lodash@^4.14.159": + version "4.14.182" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + +"@types/mocha@^9.1.1": + version "9.1.1" + resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*": + version "18.0.3" + resolved "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz" + integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + +"@types/node@^12.12.54": + version "12.20.55" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.9" + resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" + +base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + +bignumber.js@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" + integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +bs58@^4.0.0, bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + +buffer@6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz" + integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +buffer@^5.4.3: + version "5.7.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.0.6" + resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz" + integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw== + dependencies: + node-gyp-build "^4.3.0" + +byteify@^2.0.10: + version "2.0.10" + resolved "https://registry.npmjs.org/byteify/-/byteify-2.0.10.tgz" + integrity sha512-clrE0NtRB/YwjQcmrUU9qpxRIQ5Jc1HGedv6/LWd08upE0FV0S4YvPBkmKEsTaquqGmhx34LkRBO+lXpTgwYgw== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelcase@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-7.0.0.tgz" + integrity sha512-JToIvOmz6nhGsUhAYScbo2d6Py5wojjNfoxoc2mEVLUdJ70gJK2gnd+ABY1Tc3sVMyK7QDPtN0T/XdlCQWITyQ== + +chai@^4.3.4: + version "4.3.6" + resolved "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + loupe "^2.3.1" + pathval "^1.1.1" + type-detect "^4.0.5" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" + integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^2.20.3: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +elliptic@^6.5.4: + version "6.5.4" + resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" + integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +jayson@^3.4.4: + version "3.6.6" + resolved "https://registry.npmjs.org/jayson/-/jayson-3.6.6.tgz" + integrity sha512-f71uvrAWTtrwoww6MKcl9phQTC+56AopLyEenWvKVAIMz+q0oVGj6tenLZ7Z6UiPBkJtKLj4kt0tACllFQruGQ== + dependencies: + "@types/connect" "^3.4.33" + "@types/express-serve-static-core" "^4.17.9" + "@types/lodash" "^4.14.159" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + lodash "^4.17.20" + uuid "^8.3.2" + ws "^7.4.5" + +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + +js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +keccak256@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/keccak256/-/keccak256-1.0.6.tgz" + integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== + dependencies: + bn.js "^5.2.0" + buffer "^6.0.3" + keccak "^3.0.2" + +keccak@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz" + integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== + dependencies: + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.6.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.20: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@^9.0.3: + version "9.2.2" + resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + +node-fetch@2, node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: + version "4.5.0" + resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz" + integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +pako@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" + integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@^2.6.2: + version "2.7.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +react-native-url-polyfill@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz" + integrity sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ== + dependencies: + whatwg-url-without-unicode "8.0.0-3" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rpc-websockets@^7.5.0: + version "7.5.0" + resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz" + integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ== + dependencies: + "@babel/runtime" "^7.17.2" + eventemitter3 "^4.0.7" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +secp256k1@^4.0.2: + version "4.0.3" + resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superstruct@^0.14.2: + version "0.14.2" + resolved "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz" + integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== + +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz" + integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths@^3.5.0: + version "3.14.1" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +typescript@^4.3.5: + version "4.7.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +utf-8-validate@^5.0.2: + version "5.0.9" + resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz" + integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q== + dependencies: + node-gyp-build "^4.3.0" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^7.4.5: + version "7.5.8" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz" + integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== + +ws@^8.5.0: + version "8.8.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz" + integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==