sdk/js: aptos (#1736)
* sdk/js: aptos
* sdk/aptos: change api interface to be more flexible
sdk/aptos: add attestToken
sdk/aptos: added createdWrapped
sdk/aptos: add getForeignAsset
sdk/aptos: stricter sanity check for fully qualified type
sdk/aptos: ensure addresses are left padded
sdk/aptos: check if asset exists in getForeignAsset
sdk/aptos: stricter sanity check - hex prefix can't be capital
sdk/aptos: add updatewrapped
sdk/aptos: update readme with token attestation example
sdk/aptos: added transfer
sdk/aptos: add getIsTransferCompleted
sdk/aptos: add isWrappedAsset and getOriginalAsset
sdk/aptos: add redeem
sdk/aptos: make init tokenbridge entry func
* sdk/aptos: separated signing/submitting txs from creating raw txs
* clients/js: hash aptos fully qualified type to get address
* sdk/aptos: return payload from api instead of rawtx
* sdk/aptos: derive token info from vaa
* sdk/aptos: fix getAssetFullyQualifiedType for native asset
* sdk/aptos: add min gas price
* sdk/aptos: bump aptos version
* sdk/aptos: dont require 0x in front of addresses
* sdk/aptos: progress on e2e tests
* sdk/aptos: upgrade resource address derivation
This was changed recently
25696fd266/aptos-move/framework/aptos-framework/sources/account.move (L90-L95)
* sdk/js: fix getForeignAssetAptos
* sdk/js: update testnet aptos address
* sdk/js: update aptos entry functions
* sdk/aptos: fix parsesequencefromlog
* sdk/aptos: throw errors instead of string literal
* sdk/aptos: update testnet/mainnet addresses
* sdk/aptos: fix completeTransfer and getOriginalAsset
* sdk/aptos: update transferTokens to take in type and remove wormholeFee param
* sdk/aptos: add typeToExternalAddress utility
* sdk/js: update parseSequenceFromLogAptos
* sdk/js: test version bump again
* sdk/aptos: make transfer param type consistent
* sdk/aptos: test transfer to another chain test done
* sdk/aptos: use completeTransferAndRegister
* sdk/aptos: allow tryNativeToHexString to take in account addresses
* sdk/aptos: finish e2e tests
* sdk/aptos: test all apis
* sdk/aptos: add registerCoin utility
* sdk/js: utility to submit script bytecode to chain
* sdk/aptos: update test to be idempotent
* sdk/aptos: stricter check on aptos type
* clients/js: remove unused imports from rebase
* sdk/aptos: change node and faucet urls in ci
Co-authored-by: aki <akshath@live.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
This commit is contained in:
parent
91bd9a5c36
commit
eaa5107b33
|
@ -1,6 +1,7 @@
|
|||
# Wormhole CLI
|
||||
|
||||
This tool is a command line interface to Wormhole.
|
||||
|
||||
## Installation
|
||||
|
||||
make install
|
||||
|
@ -12,7 +13,7 @@ private keys, based on `.env.sample` in this folder.
|
|||
|
||||
## Usage
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
worm [command]
|
||||
|
||||
Commands:
|
||||
|
@ -31,19 +32,32 @@ Options:
|
|||
--version Show version number [boolean]
|
||||
```
|
||||
|
||||
Consult the `--help` flag for using subcommands.
|
||||
Consult the `--help` flag for using subcommands.
|
||||
|
||||
### VAA generation
|
||||
### VAA generation
|
||||
|
||||
Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
|
||||
Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
$ worm generate registration --module NFTBridge \
|
||||
--chain bsc \
|
||||
--contract-address 0x706abc4E45D419950511e474C7B9Ed348A4a716c \
|
||||
--guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
|
||||
```
|
||||
|
||||
Example creating a token attestation VAA:
|
||||
|
||||
```sh
|
||||
$ worm generate attestation --emitter-chain ethereum \
|
||||
--emitter-address 11111111111111111111111111111115 \
|
||||
--chain ethereum \
|
||||
--token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
|
||||
--decimals 6 \
|
||||
--symbol USDC \
|
||||
--name USDC \
|
||||
--guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
|
||||
```
|
||||
|
||||
### VAA parsing
|
||||
|
||||
Use `parse` to parse a VAA into JSON. For example,
|
||||
|
@ -52,7 +66,7 @@ Use `parse` to parse a VAA into JSON. For example,
|
|||
|
||||
will fetch governance VAA `13940208096455381020` and print it as JSON.
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
# ...signatures elided
|
||||
timestamp: 1651416474,
|
||||
nonce: 1570649151,
|
||||
|
@ -97,12 +111,11 @@ what's the destination chain and module. For example, a contract upgrade contain
|
|||
|
||||
worm submit $(cat my-nft-registration.txt) --network mainnet
|
||||
|
||||
|
||||
For VAAs that don't have a specific target chain (like registrations or guardian
|
||||
set upgrades), the script will ask you to specify the target chain.
|
||||
For example, to submit a guardian set upgrade on all chains, simply run:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
$ worm-fetch-governance 13940208096455381020 > guardian-upgrade.txt
|
||||
$ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain oasis
|
||||
$ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain aurora
|
||||
|
@ -121,12 +134,11 @@ $ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain celo
|
|||
|
||||
The VAA payload type (guardian set upgrade) specifies that this VAA should go to the core bridge, and the tool directs it there.
|
||||
|
||||
|
||||
### info
|
||||
|
||||
To get info about a contract (only EVM supported at this time)
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
$ worm evm info -c bsc -n mainnet -m TokenBridge
|
||||
|
||||
{
|
||||
|
@ -169,6 +181,7 @@ $ worm evm info -c bsc -n mainnet -m TokenBridge
|
|||
}
|
||||
|
||||
```
|
||||
|
||||
### Misc
|
||||
|
||||
To get the contract address for a module:
|
||||
|
@ -178,4 +191,3 @@ To get the contract address for a module:
|
|||
To get the RPC address for a chain
|
||||
|
||||
$ worm rpc mainnet bsc
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import { impossible, Payload, serialiseVAA, VAA } from "./vaa";
|
|||
import { ethers } from "ethers";
|
||||
import { NETWORKS } from "./networks";
|
||||
import base58 from "bs58";
|
||||
import { sha3_256 } from "js-sha3";
|
||||
import { isOutdated } from "./cmds/update";
|
||||
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
import { assertChain, assertEVMChain, ChainName, CHAINS, CONTRACTS as SDK_CONTRACTS, isCosmWasmChain, isEVMChain, isTerraChain, toChainId, toChainName } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
|
||||
|
@ -923,8 +924,11 @@ function parseAddress(chain: ChainName, address: string): string {
|
|||
} else if (chain === "sui") {
|
||||
throw Error("SUI is not supported yet");
|
||||
} else if (chain === "aptos") {
|
||||
// TODO: is there a better native format for aptos?
|
||||
if (/^(0x)?[0-9a-fA-F]+$/.test(address)) {
|
||||
return "0x" + evm_address(address);
|
||||
}
|
||||
|
||||
return sha3_256(Buffer.from(address)); // address is hash of fully qualified type
|
||||
} else if (chain === "wormholechain" || (chain + "") == "wormchain") {
|
||||
// TODO: update this condition after ChainName is updated to remove "wormholechain"
|
||||
const sdk = require("@certusone/wormhole-sdk/lib/cjs/utils/array")
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@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",
|
||||
"js-base64": "^3.6.1",
|
||||
|
@ -2496,10 +2497,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
|
||||
"integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
|
||||
"dev": true,
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
|
||||
"integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
@ -2568,6 +2568,32 @@
|
|||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/@scure/bip39": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
|
||||
"integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.1.1",
|
||||
"@scure/base": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
|
@ -3353,6 +3379,43 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/aptos": {
|
||||
"version": "1.3.16",
|
||||
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
|
||||
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.1.3",
|
||||
"@scure/bip39": "1.1.0",
|
||||
"axios": "0.27.2",
|
||||
"form-data": "4.0.0",
|
||||
"tweetnacl": "1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aptos/node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aptos/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
|
@ -15368,10 +15431,9 @@
|
|||
}
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
|
||||
"integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
|
||||
"dev": true
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
|
||||
"integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A=="
|
||||
},
|
||||
"@openzeppelin/contracts": {
|
||||
"version": "4.2.0",
|
||||
|
@ -15433,6 +15495,20 @@
|
|||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
|
||||
},
|
||||
"@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
||||
},
|
||||
"@scure/bip39": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
|
||||
"integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
|
||||
"requires": {
|
||||
"@noble/hashes": "~1.1.1",
|
||||
"@scure/base": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
|
@ -16092,6 +16168,39 @@
|
|||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"aptos": {
|
||||
"version": "1.3.16",
|
||||
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
|
||||
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
|
||||
"requires": {
|
||||
"@noble/hashes": "1.1.3",
|
||||
"@scure/bip39": "1.1.0",
|
||||
"axios": "0.27.2",
|
||||
"form-data": "4.0.0",
|
||||
"tweetnacl": "1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@certusone/wormhole-sdk",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.6",
|
||||
"description": "SDK for interacting with Wormhole",
|
||||
"homepage": "https://wormhole.com",
|
||||
"main": "./lib/cjs/index.js",
|
||||
|
@ -66,6 +66,7 @@
|
|||
"@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",
|
||||
"js-base64": "^3.6.1",
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { Types } from "aptos";
|
||||
|
||||
// Contract upgrade
|
||||
|
||||
export const authorizeUpgrade = (
|
||||
address: string,
|
||||
vaa: Uint8Array
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!address) throw new Error("Need bridge address.");
|
||||
return {
|
||||
function: `${address}::contract_upgrade::submit_vaa_entry`,
|
||||
type_arguments: [],
|
||||
arguments: [vaa],
|
||||
};
|
||||
};
|
||||
|
||||
export const upgradeContract = (
|
||||
address: string,
|
||||
metadataSerialized: Uint8Array,
|
||||
code: Array<Uint8Array>
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!address) throw new Error("Need bridge address.");
|
||||
return {
|
||||
function: `${address}::contract_upgrade::upgrade`,
|
||||
type_arguments: [],
|
||||
arguments: [metadataSerialized, code],
|
||||
};
|
||||
};
|
||||
|
||||
export const migrateContract = (
|
||||
address: string
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!address) throw new Error("Need bridge address.");
|
||||
return {
|
||||
function: `${address}::contract_upgrade::migrate`,
|
||||
type_arguments: [],
|
||||
arguments: [],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import { Types } from "aptos";
|
||||
import { ChainId } from "../../utils";
|
||||
|
||||
// Guardian set upgrade
|
||||
|
||||
export const upgradeGuardianSet = (
|
||||
coreBridgeAddress: string,
|
||||
vaa: Uint8Array,
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!coreBridgeAddress) throw new Error("Need core bridge address.");
|
||||
return {
|
||||
function: `${coreBridgeAddress}::guardian_set_upgrade::submit_vaa_entry`,
|
||||
type_arguments: [],
|
||||
arguments: [vaa],
|
||||
};
|
||||
};
|
||||
|
||||
// Init WH
|
||||
|
||||
export const initWormhole = (
|
||||
coreBridgeAddress: string,
|
||||
chainId: ChainId,
|
||||
governanceChainId: number,
|
||||
governanceContract: Uint8Array,
|
||||
initialGuardian: Uint8Array,
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!coreBridgeAddress) throw new Error("Need core bridge address.");
|
||||
return {
|
||||
function: `${coreBridgeAddress}::wormhole::init`,
|
||||
type_arguments: [],
|
||||
arguments: [chainId, governanceChainId, governanceContract, initialGuardian],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,241 @@
|
|||
import { AptosClient, TxnBuilderTypes, Types } from "aptos";
|
||||
import { _parseVAAAlgorand } from "../../algorand";
|
||||
import {
|
||||
assertChain,
|
||||
ChainId,
|
||||
ChainName,
|
||||
CHAIN_ID_APTOS,
|
||||
coalesceChainId,
|
||||
getAssetFullyQualifiedType,
|
||||
getTypeFromExternalAddress,
|
||||
hexToUint8Array,
|
||||
isValidAptosType,
|
||||
} from "../../utils";
|
||||
|
||||
// Attest token
|
||||
|
||||
export const attestToken = (
|
||||
tokenBridgeAddress: string,
|
||||
tokenChain: ChainId | ChainName,
|
||||
tokenAddress: string,
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
const assetType = getAssetFullyQualifiedType(
|
||||
tokenBridgeAddress,
|
||||
coalesceChainId(tokenChain),
|
||||
tokenAddress,
|
||||
);
|
||||
if (!assetType) throw new Error("Invalid asset address.");
|
||||
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::attest_token::attest_token_entry`,
|
||||
type_arguments: [assetType],
|
||||
arguments: [],
|
||||
};
|
||||
};
|
||||
|
||||
// Complete transfer
|
||||
|
||||
export const completeTransfer = async (
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
transferVAA: Uint8Array,
|
||||
feeRecipient: string,
|
||||
): Promise<Types.EntryFunctionPayload> => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
|
||||
const parsedVAA = _parseVAAAlgorand(transferVAA);
|
||||
if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
|
||||
throw new Error("VAA does not contain required information");
|
||||
}
|
||||
|
||||
if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
|
||||
throw new Error("Transfer is not destined for Aptos");
|
||||
}
|
||||
|
||||
assertChain(parsedVAA.FromChain);
|
||||
const assetType =
|
||||
parsedVAA.FromChain === CHAIN_ID_APTOS
|
||||
? await getTypeFromExternalAddress(
|
||||
client,
|
||||
tokenBridgeAddress,
|
||||
parsedVAA.Contract
|
||||
)
|
||||
: getAssetFullyQualifiedType(
|
||||
tokenBridgeAddress,
|
||||
coalesceChainId(parsedVAA.FromChain),
|
||||
parsedVAA.Contract
|
||||
);
|
||||
if (!assetType) throw new Error("Invalid asset address.");
|
||||
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_entry`,
|
||||
type_arguments: [assetType],
|
||||
arguments: [transferVAA, feeRecipient],
|
||||
};
|
||||
};
|
||||
|
||||
export const completeTransferAndRegister = async (
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
transferVAA: Uint8Array,
|
||||
): Promise<Types.EntryFunctionPayload> => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
|
||||
const parsedVAA = _parseVAAAlgorand(transferVAA);
|
||||
if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
|
||||
throw new Error("VAA does not contain required information");
|
||||
}
|
||||
|
||||
if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
|
||||
throw new Error("Transfer is not destined for Aptos");
|
||||
}
|
||||
|
||||
assertChain(parsedVAA.FromChain);
|
||||
const assetType =
|
||||
parsedVAA.FromChain === CHAIN_ID_APTOS
|
||||
? await getTypeFromExternalAddress(
|
||||
client,
|
||||
tokenBridgeAddress,
|
||||
parsedVAA.Contract
|
||||
)
|
||||
: getAssetFullyQualifiedType(
|
||||
tokenBridgeAddress,
|
||||
coalesceChainId(parsedVAA.FromChain),
|
||||
parsedVAA.Contract
|
||||
);
|
||||
if (!assetType) throw new Error("Invalid asset address.");
|
||||
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_and_register_entry`,
|
||||
type_arguments: [assetType],
|
||||
arguments: [transferVAA],
|
||||
};
|
||||
};
|
||||
|
||||
export const completeTransferWithPayload = (
|
||||
_tokenBridgeAddress: string,
|
||||
_tokenChain: ChainId | ChainName,
|
||||
_tokenAddress: string,
|
||||
_vaa: Uint8Array,
|
||||
): Types.EntryFunctionPayload => {
|
||||
throw new Error("Completing transfers with payload is not yet supported in the sdk");
|
||||
};
|
||||
|
||||
export const registerCoin = (
|
||||
tokenBridgeAddress: string,
|
||||
originChain: ChainId | ChainName,
|
||||
originAddress: string
|
||||
): TxnBuilderTypes.TransactionPayloadScript => {
|
||||
const bytecode = hexToUint8Array(
|
||||
"a11ceb0b050000000601000403041104150405190b072436085a200000000101020002000003020401000004000101000103020301060c000105010900010104636f696e067369676e65720a616464726573735f6f661569735f6163636f756e745f726567697374657265640872656769737465720000000000000000000000000000000000000000000000000000000000000001010000010c0a001100380020030605090b003801050b0b000102"
|
||||
);
|
||||
const assetType = getAssetFullyQualifiedType(
|
||||
tokenBridgeAddress,
|
||||
coalesceChainId(originChain),
|
||||
originAddress
|
||||
);
|
||||
if (!assetType) throw new Error("Asset type is null");
|
||||
const typeTag = new TxnBuilderTypes.TypeTagStruct(
|
||||
TxnBuilderTypes.StructTag.fromString(assetType)
|
||||
);
|
||||
|
||||
return new TxnBuilderTypes.TransactionPayloadScript(
|
||||
new TxnBuilderTypes.Script(bytecode, [typeTag], [])
|
||||
);
|
||||
};
|
||||
|
||||
// Deploy coin
|
||||
|
||||
// don't need `signer` and `&signer` in argument list because the Move VM will inject them
|
||||
export const deployCoin = (tokenBridgeAddress: string): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::deploy_coin::deploy_coin`,
|
||||
type_arguments: [],
|
||||
arguments: [],
|
||||
};
|
||||
};
|
||||
|
||||
// Register chain
|
||||
|
||||
export const registerChain = (
|
||||
tokenBridgeAddress: string,
|
||||
vaa: Uint8Array,
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::register_chain::submit_vaa_entry`,
|
||||
type_arguments: [],
|
||||
arguments: [vaa],
|
||||
};
|
||||
};
|
||||
|
||||
// Transfer tokens
|
||||
|
||||
export const transferTokens = (
|
||||
tokenBridgeAddress: string,
|
||||
fullyQualifiedType: string,
|
||||
amount: string,
|
||||
recipientChain: ChainId | ChainName,
|
||||
recipient: Uint8Array,
|
||||
relayerFee: string,
|
||||
nonce: number,
|
||||
payload: string = "",
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
if (!isValidAptosType(fullyQualifiedType)) {
|
||||
throw new Error("Need fully qualified address");
|
||||
}
|
||||
|
||||
const recipientChainId = coalesceChainId(recipientChain);
|
||||
if (payload) {
|
||||
throw new Error("Transfer with payload are not yet supported in the sdk");
|
||||
} else {
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::transfer_tokens::transfer_tokens_entry`,
|
||||
type_arguments: [fullyQualifiedType],
|
||||
arguments: [amount, recipientChainId, recipient, relayerFee, nonce],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Created wrapped coin
|
||||
|
||||
export const createWrappedCoinType = (
|
||||
tokenBridgeAddress: string,
|
||||
vaa: Uint8Array,
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin_type`,
|
||||
type_arguments: [],
|
||||
arguments: [vaa],
|
||||
};
|
||||
};
|
||||
|
||||
export const createWrappedCoin = (
|
||||
tokenBridgeAddress: string,
|
||||
attestVAA: Uint8Array
|
||||
): Types.EntryFunctionPayload => {
|
||||
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
|
||||
|
||||
const parsedVAA = _parseVAAAlgorand(attestVAA);
|
||||
if (!parsedVAA.FromChain || !parsedVAA.Contract) {
|
||||
throw new Error("VAA does not contain required information");
|
||||
}
|
||||
|
||||
assertChain(parsedVAA.FromChain);
|
||||
const assetType = getAssetFullyQualifiedType(
|
||||
tokenBridgeAddress,
|
||||
coalesceChainId(parsedVAA.FromChain),
|
||||
parsedVAA.Contract
|
||||
);
|
||||
if (!assetType) throw new Error("Invalid asset address.");
|
||||
|
||||
return {
|
||||
function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin`,
|
||||
type_arguments: [assetType],
|
||||
arguments: [attestVAA],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./api/common";
|
||||
export * from "./api/coreBridge";
|
||||
export * from "./api/tokenBridge";
|
|
@ -0,0 +1,38 @@
|
|||
export type State = {
|
||||
consumed_vaas: {
|
||||
elems: {
|
||||
handle: string;
|
||||
};
|
||||
};
|
||||
emitter_cap: {
|
||||
emitter: string;
|
||||
sequence: string;
|
||||
};
|
||||
governance_chain_id: {
|
||||
number: string;
|
||||
};
|
||||
governance_contract: {
|
||||
external_address: string;
|
||||
};
|
||||
native_infos: {
|
||||
handle: string;
|
||||
};
|
||||
registered_emitters: {
|
||||
handle: string;
|
||||
};
|
||||
signer_cap: {
|
||||
account: string;
|
||||
};
|
||||
wrapped_infos: {
|
||||
handle: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type OriginInfo = {
|
||||
token_address: {
|
||||
external_address: string;
|
||||
};
|
||||
token_chain: {
|
||||
number: string; // lol
|
||||
};
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { TransactionResponse } from "@solana/web3.js";
|
||||
import { TxInfo } from "@terra-money/terra.js";
|
||||
import { TxInfo as XplaTxInfo } from "@xpla/xpla.js";
|
||||
import { AptosClient, Types } from "aptos";
|
||||
import { BigNumber, ContractReceipt } from "ethers";
|
||||
import { FinalExecutionOutcome } from "near-api-js/lib/providers";
|
||||
import { Implementation__factory } from "../ethers-contracts";
|
||||
|
@ -159,3 +160,23 @@ export function parseSequenceFromLogNear(
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a transaction result, return the first WormholeMessage event sequence
|
||||
* @param coreBridgeAddress Wormhole Core bridge address
|
||||
* @param result the result of client.waitForTransactionWithResult(txHash)
|
||||
* @returns sequence
|
||||
*/
|
||||
export function parseSequenceFromLogAptos(
|
||||
coreBridgeAddress: string,
|
||||
result: Types.UserTransaction
|
||||
): string | null {
|
||||
if (result.success) {
|
||||
const event = result.events.find(
|
||||
(e) => e.type === `${coreBridgeAddress}::state::WormholeMessage`
|
||||
);
|
||||
return event?.data.sequence || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,410 @@
|
|||
import { describe, expect, jest, test } from "@jest/globals";
|
||||
import {
|
||||
AptosAccount,
|
||||
AptosClient,
|
||||
FaucetClient,
|
||||
HexString,
|
||||
Types,
|
||||
} from "aptos";
|
||||
import {
|
||||
approveEth,
|
||||
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
|
||||
attestFromAptos,
|
||||
attestFromEth,
|
||||
CHAIN_ID_APTOS,
|
||||
CHAIN_ID_ETH,
|
||||
CONTRACTS,
|
||||
createWrappedOnAptos,
|
||||
createWrappedOnEth,
|
||||
createWrappedTypeOnAptos,
|
||||
getAssetFullyQualifiedType,
|
||||
getEmitterAddressEth,
|
||||
getExternalAddressFromType,
|
||||
getForeignAssetAptos,
|
||||
getForeignAssetEth,
|
||||
getIsTransferCompletedAptos,
|
||||
getIsTransferCompletedEth,
|
||||
getIsWrappedAssetAptos,
|
||||
getOriginalAssetAptos,
|
||||
getSignedVAAWithRetry,
|
||||
hexToUint8Array,
|
||||
redeemOnAptos,
|
||||
redeemOnEth,
|
||||
signAndSubmitEntryFunction,
|
||||
signAndSubmitScript,
|
||||
TokenImplementation__factory,
|
||||
transferFromAptos,
|
||||
transferFromEth,
|
||||
tryNativeToHexString,
|
||||
tryNativeToUint8Array,
|
||||
uint8ArrayToHex,
|
||||
} from "../..";
|
||||
import { setDefaultWasm } from "../../solana/wasm";
|
||||
import {
|
||||
APTOS_FAUCET_URL,
|
||||
APTOS_NODE_URL,
|
||||
APTOS_PRIVATE_KEY,
|
||||
ETH_NODE_URL,
|
||||
ETH_PRIVATE_KEY3,
|
||||
TEST_ERC20,
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
} from "./consts";
|
||||
import {
|
||||
parseSequenceFromLogAptos,
|
||||
parseSequenceFromLogEth,
|
||||
} from "../../bridge/parseSequenceFromLog";
|
||||
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");
|
||||
|
||||
const JEST_TEST_TIMEOUT = 60000;
|
||||
jest.setTimeout(JEST_TEST_TIMEOUT);
|
||||
|
||||
describe("Aptos SDK tests", () => {
|
||||
test("Transfer native token from Aptos to Ethereum", async () => {
|
||||
// setup aptos
|
||||
const client = new AptosClient(APTOS_NODE_URL);
|
||||
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
|
||||
const sender = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
|
||||
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
|
||||
const aptosCoreBridge = CONTRACTS.DEVNET.aptos.core;
|
||||
|
||||
// sanity check funds in the account
|
||||
const COIN_TYPE = "0x1::aptos_coin::AptosCoin";
|
||||
const before = await getBalanceAptos(client, COIN_TYPE, sender.address());
|
||||
await faucet.fundAccount(sender.address(), 100_000_000);
|
||||
const after = await getBalanceAptos(client, COIN_TYPE, sender.address());
|
||||
expect(Number(after) - Number(before)).toEqual(100_000_000);
|
||||
|
||||
// attest native aptos token
|
||||
const attestPayload = attestFromAptos(
|
||||
aptosTokenBridge,
|
||||
CHAIN_ID_APTOS,
|
||||
COIN_TYPE
|
||||
);
|
||||
let tx = (await signAndSubmitEntryFunction(
|
||||
client,
|
||||
sender,
|
||||
attestPayload
|
||||
)) as Types.UserTransaction;
|
||||
|
||||
// get signed attest vaa
|
||||
let sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
|
||||
expect(sequence).toBeTruthy();
|
||||
|
||||
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_APTOS,
|
||||
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
|
||||
sequence!,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
},
|
||||
1000,
|
||||
5
|
||||
);
|
||||
expect(attestVAA).toBeTruthy();
|
||||
|
||||
// setup ethereum
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const recipient = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
|
||||
const recipientAddress = await recipient.getAddress();
|
||||
const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
|
||||
try {
|
||||
await createWrappedOnEth(ethTokenBridge, recipient, attestVAA);
|
||||
} catch (e) {
|
||||
// this could fail because the token is already attested (in an unclean env)
|
||||
}
|
||||
|
||||
// check attestation on ethereum
|
||||
const externalAddress = hexToUint8Array(
|
||||
await getExternalAddressFromType(COIN_TYPE)
|
||||
);
|
||||
const address = getForeignAssetEth(
|
||||
ethTokenBridge,
|
||||
provider,
|
||||
CHAIN_ID_APTOS,
|
||||
externalAddress
|
||||
);
|
||||
expect(address).toBeTruthy();
|
||||
expect(address).not.toBe(ethers.constants.AddressZero);
|
||||
|
||||
// transfer from aptos
|
||||
const balanceBeforeTransferAptos = ethers.BigNumber.from(
|
||||
await getBalanceAptos(client, COIN_TYPE, sender.address())
|
||||
);
|
||||
const transferPayload = transferFromAptos(
|
||||
aptosTokenBridge,
|
||||
COIN_TYPE,
|
||||
(10_000_000).toString(),
|
||||
CHAIN_ID_ETH,
|
||||
tryNativeToUint8Array(recipientAddress, CHAIN_ID_ETH)
|
||||
);
|
||||
tx = (await signAndSubmitEntryFunction(
|
||||
client,
|
||||
sender,
|
||||
transferPayload
|
||||
)) as Types.UserTransaction;
|
||||
const balanceAfterTransferAptos = ethers.BigNumber.from(
|
||||
await getBalanceAptos(client, COIN_TYPE, sender.address())
|
||||
);
|
||||
expect(
|
||||
balanceBeforeTransferAptos
|
||||
.sub(balanceAfterTransferAptos)
|
||||
.gt((10_000_000).toString())
|
||||
).toBe(true);
|
||||
|
||||
// get signed transfer vaa
|
||||
sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
|
||||
expect(sequence).toBeTruthy();
|
||||
|
||||
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_APTOS,
|
||||
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
|
||||
sequence!,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
},
|
||||
1000,
|
||||
5
|
||||
);
|
||||
expect(transferVAA).toBeTruthy();
|
||||
|
||||
// get balance on eth
|
||||
const originAssetHex = tryNativeToUint8Array(COIN_TYPE, CHAIN_ID_APTOS);
|
||||
if (!originAssetHex) {
|
||||
throw new Error("originAssetHex is null");
|
||||
}
|
||||
|
||||
const foreignAsset = await getForeignAssetEth(
|
||||
ethTokenBridge,
|
||||
provider,
|
||||
CHAIN_ID_APTOS,
|
||||
originAssetHex
|
||||
);
|
||||
if (!foreignAsset) {
|
||||
throw new Error("foreignAsset is null");
|
||||
}
|
||||
|
||||
const balanceBeforeTransferEth = await getBalanceEth(
|
||||
foreignAsset,
|
||||
recipient
|
||||
);
|
||||
|
||||
// redeem on eth
|
||||
await redeemOnEth(ethTokenBridge, recipient, transferVAA);
|
||||
expect(
|
||||
await getIsTransferCompletedEth(ethTokenBridge, provider, transferVAA)
|
||||
).toBe(true);
|
||||
const balanceAfterTransferEth = await getBalanceEth(
|
||||
foreignAsset,
|
||||
recipient
|
||||
);
|
||||
expect(
|
||||
balanceAfterTransferEth.sub(balanceBeforeTransferEth).toNumber()
|
||||
).toEqual(10_000_000);
|
||||
|
||||
// clean up
|
||||
provider.destroy();
|
||||
});
|
||||
test("Transfer native ERC-20 from Ethereum to Aptos", async () => {
|
||||
// setup ethereum
|
||||
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
const sender = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
|
||||
const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
|
||||
const ethCoreBridge = CONTRACTS.DEVNET.ethereum.core;
|
||||
|
||||
// attest from eth
|
||||
const attestReceipt = await attestFromEth(
|
||||
ethTokenBridge,
|
||||
sender,
|
||||
TEST_ERC20
|
||||
);
|
||||
|
||||
// get signed attest vaa
|
||||
let sequence = parseSequenceFromLogEth(attestReceipt, ethCoreBridge);
|
||||
expect(sequence).toBeTruthy();
|
||||
|
||||
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
getEmitterAddressEth(ethTokenBridge),
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
},
|
||||
1000,
|
||||
5
|
||||
);
|
||||
expect(attestVAA).toBeTruthy();
|
||||
|
||||
// setup aptos
|
||||
const client = new AptosClient(APTOS_NODE_URL);
|
||||
const recipient = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
|
||||
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
|
||||
const createWrappedCoinTypePayload = createWrappedTypeOnAptos(
|
||||
aptosTokenBridge,
|
||||
attestVAA
|
||||
);
|
||||
try {
|
||||
await signAndSubmitEntryFunction(
|
||||
client,
|
||||
recipient,
|
||||
createWrappedCoinTypePayload
|
||||
);
|
||||
} catch (e) {
|
||||
// only throw if token has not been attested but this call fails
|
||||
if (
|
||||
!(
|
||||
new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
|
||||
new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
|
||||
)
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const createWrappedCoinPayload = createWrappedOnAptos(
|
||||
aptosTokenBridge,
|
||||
attestVAA
|
||||
);
|
||||
try {
|
||||
await signAndSubmitEntryFunction(
|
||||
client,
|
||||
recipient,
|
||||
createWrappedCoinPayload
|
||||
);
|
||||
} catch (e) {
|
||||
// only throw if token has not been attested but this call fails
|
||||
if (
|
||||
!(
|
||||
new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
|
||||
new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
|
||||
)
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// check attestation on aptos
|
||||
const aptosWrappedAddress = await getForeignAssetAptos(
|
||||
client,
|
||||
aptosTokenBridge,
|
||||
CHAIN_ID_ETH,
|
||||
TEST_ERC20
|
||||
);
|
||||
if (!aptosWrappedAddress) {
|
||||
throw new Error("Failed to create wrapped coin on Aptos");
|
||||
}
|
||||
|
||||
const wrappedType = getAssetFullyQualifiedType(
|
||||
aptosTokenBridge,
|
||||
CHAIN_ID_ETH,
|
||||
TEST_ERC20
|
||||
);
|
||||
if (!wrappedType) {
|
||||
throw new Error("wrappedType is null");
|
||||
}
|
||||
|
||||
const info = await getOriginalAssetAptos(
|
||||
client,
|
||||
aptosTokenBridge,
|
||||
wrappedType
|
||||
);
|
||||
expect(uint8ArrayToHex(info.assetAddress)).toEqual(
|
||||
tryNativeToHexString(TEST_ERC20, CHAIN_ID_ETH)
|
||||
);
|
||||
expect(info.chainId).toEqual(CHAIN_ID_ETH);
|
||||
expect(info.isWrapped).toEqual(
|
||||
await getIsWrappedAssetAptos(
|
||||
client,
|
||||
aptosTokenBridge,
|
||||
aptosWrappedAddress
|
||||
)
|
||||
);
|
||||
|
||||
// transfer from eth
|
||||
const balanceBeforeTransferEth = await getBalanceEth(TEST_ERC20, sender);
|
||||
const amount = parseUnits("1", 18);
|
||||
await approveEth(ethTokenBridge, TEST_ERC20, sender, amount);
|
||||
const transferReceipt = await transferFromEth(
|
||||
ethTokenBridge,
|
||||
sender,
|
||||
TEST_ERC20,
|
||||
amount,
|
||||
CHAIN_ID_APTOS,
|
||||
tryNativeToUint8Array(recipient.address().hex(), CHAIN_ID_APTOS)
|
||||
);
|
||||
|
||||
// get signed transfer vaa
|
||||
sequence = parseSequenceFromLogEth(transferReceipt, ethCoreBridge);
|
||||
expect(sequence).toBeTruthy();
|
||||
|
||||
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
|
||||
WORMHOLE_RPC_HOSTS,
|
||||
CHAIN_ID_ETH,
|
||||
getEmitterAddressEth(ethTokenBridge),
|
||||
sequence,
|
||||
{
|
||||
transport: NodeHttpTransport(),
|
||||
},
|
||||
1000,
|
||||
5
|
||||
);
|
||||
expect(transferVAA).toBeTruthy();
|
||||
|
||||
// register token on aptos
|
||||
const script = registerCoin(aptosTokenBridge, CHAIN_ID_ETH, TEST_ERC20);
|
||||
await signAndSubmitScript(client, recipient, script);
|
||||
|
||||
// redeem on aptos
|
||||
const balanceBeforeTransferAptos = ethers.BigNumber.from(
|
||||
await getBalanceAptos(client, wrappedType, recipient.address())
|
||||
);
|
||||
const redeemPayload = await redeemOnAptos(
|
||||
client,
|
||||
aptosTokenBridge,
|
||||
transferVAA
|
||||
);
|
||||
await signAndSubmitEntryFunction(client, recipient, redeemPayload);
|
||||
expect(
|
||||
await getIsTransferCompletedAptos(client, aptosTokenBridge, transferVAA)
|
||||
).toBe(true);
|
||||
|
||||
// check balances
|
||||
const balanceAfterTransferAptos = ethers.BigNumber.from(
|
||||
await getBalanceAptos(client, wrappedType, recipient.address())
|
||||
);
|
||||
expect(
|
||||
balanceAfterTransferAptos.sub(balanceBeforeTransferAptos).toString()
|
||||
).toEqual(parseUnits("1", 8).toString()); // max decimals is 8
|
||||
const balanceAfterTransferEth = await getBalanceEth(TEST_ERC20, sender);
|
||||
expect(
|
||||
balanceBeforeTransferEth.sub(balanceAfterTransferEth).toString()
|
||||
).toEqual(amount.toString());
|
||||
|
||||
// clean up
|
||||
provider.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
const getBalanceAptos = async (
|
||||
client: AptosClient,
|
||||
type: string,
|
||||
address: HexString
|
||||
): Promise<string> => {
|
||||
const res = await client.getAccountResource(
|
||||
address,
|
||||
`0x1::coin::CoinStore<${type}>`
|
||||
);
|
||||
return (res.data as any).coin.value;
|
||||
};
|
||||
|
||||
const getBalanceEth = (tokenAddress: string, wallet: ethers.Wallet) => {
|
||||
let token = TokenImplementation__factory.connect(tokenAddress, wallet);
|
||||
return token.balanceOf(wallet.address);
|
||||
};
|
|
@ -85,6 +85,10 @@ export const TERRA_HOST =
|
|||
|
||||
export const NEAR_NODE_URL = ci ? "http://near:3030" : "http://localhost:3030";
|
||||
|
||||
export const APTOS_NODE_URL = ci ? "http://aptos:8080/v1" : "http://0.0.0.0:8080/v1"
|
||||
export const APTOS_FAUCET_URL = ci ? "http://aptos:8081" : "http://0.0.0.0:8081"
|
||||
export const APTOS_PRIVATE_KEY = "537c1f91e56891445b491068f519b705f8c0f1a1e66111816dd5d4aa85b8113d";
|
||||
|
||||
describe("consts should exist", () => {
|
||||
it("has Solana test token", () => {
|
||||
expect.assertions(1);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { importTokenWasm } from "../solana/wasm";
|
|||
import {
|
||||
callFunctionNear,
|
||||
hashAccount,
|
||||
ChainId,
|
||||
textToHexString,
|
||||
textToUint8Array,
|
||||
uint8ArrayToHex,
|
||||
|
@ -32,6 +33,8 @@ import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm";
|
|||
import { Provider } from "near-api-js/lib/providers";
|
||||
import { FunctionCallOptions } from "near-api-js/lib/account";
|
||||
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
|
||||
import { Types } from "aptos";
|
||||
import { attestToken as attestTokenAptos } from "../aptos";
|
||||
|
||||
export async function attestFromEth(
|
||||
tokenBridgeAddress: string,
|
||||
|
@ -319,3 +322,13 @@ export async function attestNearFromNear(
|
|||
gas: new BN("100000000000000"),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: do we want to pass in a single assetAddress (instead of tokenChain and tokenAddress) as
|
||||
// with other APIs above and let user derive the wrapped asset address themselves?
|
||||
export function attestFromAptos(
|
||||
tokenBridgeAddress: string,
|
||||
tokenChain: ChainId,
|
||||
tokenAddress: string
|
||||
): Types.EntryFunctionPayload {
|
||||
return attestTokenAptos(tokenBridgeAddress, tokenChain, tokenAddress);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { Connection, PublicKey, 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, _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 BN from "bn.js";
|
||||
import { FunctionCallOptions } from "near-api-js/lib/account";
|
||||
import { Provider } from "near-api-js/lib/providers";
|
||||
import { callFunctionNear } from "../utils";
|
||||
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
|
||||
import {
|
||||
createWrappedCoin as createWrappedCoinAptos,
|
||||
createWrappedCoinType as createWrappedCoinTypeAptos,
|
||||
} from "../aptos";
|
||||
|
||||
export async function createWrappedOnEth(
|
||||
tokenBridgeAddress: string,
|
||||
|
@ -61,12 +66,7 @@ export async function createWrappedOnSolana(
|
|||
): Promise<Transaction> {
|
||||
const { create_wrapped_ix } = await importTokenWasm();
|
||||
const ix = ixFromRust(
|
||||
create_wrapped_ix(
|
||||
tokenBridgeAddress,
|
||||
bridgeAddress,
|
||||
payerAddress,
|
||||
signedVAA
|
||||
)
|
||||
create_wrapped_ix(tokenBridgeAddress, bridgeAddress, payerAddress, signedVAA)
|
||||
);
|
||||
const transaction = new Transaction().add(ix);
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
|
@ -82,13 +82,7 @@ export async function createWrappedOnAlgorand(
|
|||
senderAddr: string,
|
||||
attestVAA: Uint8Array
|
||||
): Promise<TransactionSignerPair[]> {
|
||||
return await _submitVAAAlgorand(
|
||||
client,
|
||||
tokenBridgeId,
|
||||
bridgeId,
|
||||
attestVAA,
|
||||
senderAddr
|
||||
);
|
||||
return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, attestVAA, senderAddr);
|
||||
}
|
||||
|
||||
export async function createWrappedOnNear(
|
||||
|
@ -114,3 +108,17 @@ export async function createWrappedOnNear(
|
|||
msgs.push({ ...msgs[0] });
|
||||
return msgs;
|
||||
}
|
||||
|
||||
export function createWrappedTypeOnAptos(
|
||||
tokenBridgeAddress: string,
|
||||
signedVAA: Uint8Array
|
||||
): Types.EntryFunctionPayload {
|
||||
return createWrappedCoinTypeAptos(tokenBridgeAddress, signedVAA);
|
||||
}
|
||||
|
||||
export function createWrappedOnAptos(
|
||||
tokenBridgeAddress: string,
|
||||
attestVAA: Uint8Array
|
||||
): Types.EntryFunctionPayload {
|
||||
return createWrappedCoinAptos(tokenBridgeAddress, attestVAA);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,10 @@ import { Connection, PublicKey } 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 {
|
||||
|
@ -17,6 +14,8 @@ import {
|
|||
ChainName,
|
||||
CHAIN_ID_ALGORAND,
|
||||
coalesceChainId,
|
||||
ensureHexPrefix,
|
||||
getForeignAssetAddress,
|
||||
} from "../utils";
|
||||
import { Provider } from "near-api-js/lib/providers";
|
||||
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
|
||||
|
@ -37,10 +36,7 @@ export async function getForeignAssetEth(
|
|||
): Promise<string | null> {
|
||||
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;
|
||||
}
|
||||
|
@ -53,15 +49,12 @@ export async function getForeignAssetTerra(
|
|||
originAsset: Uint8Array
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const result: { address: string } = await client.wasm.contractQuery(
|
||||
tokenBridgeAddress,
|
||||
{
|
||||
const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, {
|
||||
wrapped_registry: {
|
||||
chain: coalesceChainId(originChain),
|
||||
address: fromUint8Array(originAsset),
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
return result.address;
|
||||
} catch (e) {
|
||||
return null;
|
||||
|
@ -149,9 +142,7 @@ export async function getForeignAssetSolana(
|
|||
coalesceChainId(originChain)
|
||||
);
|
||||
const wrappedAddressPK = new PublicKey(wrappedAddress);
|
||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(
|
||||
wrappedAddressPK
|
||||
);
|
||||
const wrappedAssetAccountInfo = await connection.getAccountInfo(wrappedAddressPK);
|
||||
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
|
||||
}
|
||||
|
||||
|
@ -174,11 +165,7 @@ 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);
|
||||
|
@ -203,3 +190,24 @@ export async function getForeignAssetNear(
|
|||
);
|
||||
return ret !== "" ? ret : null;
|
||||
}
|
||||
|
||||
export async function getForeignAssetAptos(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
originChain: ChainId | ChainName,
|
||||
originAddress: string
|
||||
): Promise<string | null> {
|
||||
const originChainId = coalesceChainId(originChain);
|
||||
const assetAddress = getForeignAssetAddress(tokenBridgeAddress, originChainId, originAddress);
|
||||
if (!assetAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// check if asset exists and throw if it doesn't
|
||||
await client.getAccountResource(assetAddress, `0x1::coin::CoinInfo<${ensureHexPrefix(assetAddress)}::coin::T>`);
|
||||
return assetAddress;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
|
|||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { Algodv2, bigIntToBytes } from "algosdk";
|
||||
import { AptosClient } from "aptos";
|
||||
import axios from "axios";
|
||||
import { ethers } from "ethers";
|
||||
import { fromUint8Array } from "js-base64";
|
||||
import { redeemOnTerra } from ".";
|
||||
import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
|
||||
import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
|
||||
import {
|
||||
BITS_PER_KEY,
|
||||
calcLogicSigAccount,
|
||||
|
@ -20,11 +21,12 @@ import { importCoreWasm } from "../solana/wasm";
|
|||
import { safeBigIntToNumber } from "../utils/bigint";
|
||||
import { Provider } from "near-api-js/lib/providers";
|
||||
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
|
||||
import { State } from "../aptos/types";
|
||||
|
||||
export async function getIsTransferCompletedEth(
|
||||
tokenBridgeAddress: string,
|
||||
provider: ethers.Signer | ethers.providers.Provider,
|
||||
signedVAA: Uint8Array
|
||||
signedVAA: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
|
||||
const signedVAAHash = await getSignedVAAHash(signedVAA);
|
||||
|
@ -38,18 +40,16 @@ export async function getIsTransferCompletedTerra(
|
|||
tokenBridgeAddress: string,
|
||||
signedVAA: Uint8Array,
|
||||
client: LCDClient,
|
||||
gasPriceUrl: string
|
||||
gasPriceUrl: string,
|
||||
): Promise<boolean> {
|
||||
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 +63,7 @@ export async function getIsTransferCompletedTerra(
|
|||
memo: "already redeemed calculation",
|
||||
feeDenoms: ["uluna"],
|
||||
gasPrices,
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e: any) {
|
||||
// redeemed if the VAA was already executed
|
||||
|
@ -136,28 +136,22 @@ export async function getIsTransferCompletedXpla(
|
|||
signedVAA: Uint8Array,
|
||||
client: XplaLCDClient
|
||||
): Promise<boolean> {
|
||||
const result: { is_redeemed: boolean } = await client.wasm.contractQuery(
|
||||
tokenBridgeAddress,
|
||||
{
|
||||
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,
|
||||
connection: Connection
|
||||
connection: Connection,
|
||||
): Promise<boolean> {
|
||||
const { claim_address } = await importCoreWasm();
|
||||
const claimAddress = await claim_address(tokenBridgeAddress, signedVAA);
|
||||
const claimInfo = await connection.getAccountInfo(
|
||||
new PublicKey(claimAddress),
|
||||
"confirmed"
|
||||
);
|
||||
const claimInfo = await connection.getAccountInfo(new PublicKey(claimAddress), "confirmed");
|
||||
return !!claimInfo;
|
||||
}
|
||||
|
||||
|
@ -175,7 +169,7 @@ async function checkBitsSet(
|
|||
client: Algodv2,
|
||||
appId: bigint,
|
||||
addr: string,
|
||||
seq: bigint
|
||||
seq: bigint,
|
||||
): Promise<boolean> {
|
||||
let retval: boolean = false;
|
||||
let appState: any[] = [];
|
||||
|
@ -222,7 +216,7 @@ async function checkBitsSet(
|
|||
export async function getIsTransferCompletedAlgorand(
|
||||
client: Algodv2,
|
||||
appId: bigint,
|
||||
signedVAA: Uint8Array
|
||||
signedVAA: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
const parsedVAA = _parseVAAAlgorand(signedVAA);
|
||||
const seq: bigint = parsedVAA.sequence;
|
||||
|
@ -232,7 +226,7 @@ export async function getIsTransferCompletedAlgorand(
|
|||
client,
|
||||
appId,
|
||||
seq / BigInt(MAX_BITS),
|
||||
chainRaw + em
|
||||
chainRaw + em,
|
||||
);
|
||||
if (!doesExist) {
|
||||
return false;
|
||||
|
@ -245,7 +239,7 @@ export async function getIsTransferCompletedAlgorand(
|
|||
export async function getIsTransferCompletedNear(
|
||||
provider: Provider,
|
||||
tokenBridge: string,
|
||||
signedVAA: Uint8Array
|
||||
signedVAA: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
const vaa = Buffer.from(signedVAA).toString("hex");
|
||||
return (
|
||||
|
@ -254,3 +248,30 @@ export async function getIsTransferCompletedNear(
|
|||
})
|
||||
)[1];
|
||||
}
|
||||
|
||||
export async function getIsTransferCompletedAptos(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
signedVAA: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
// get handle
|
||||
tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
|
||||
const 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 signedVAAHash = await getSignedVAAHash(signedVAA);
|
||||
try {
|
||||
// when accessing Set<T>, key is type T and value is 0
|
||||
await client.getTableItem(handle, {
|
||||
key_type: "vector<u8>",
|
||||
value_type: "u8",
|
||||
key: signedVAAHash,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
|
|||
import { Connection, PublicKey } 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, tryNativeToHexString } from "../utils";
|
||||
import { CHAIN_ID_INJECTIVE, ensureHexPrefix, tryNativeToHexString } from "../utils";
|
||||
import { safeBigIntToNumber } from "../utils/bigint";
|
||||
import { getForeignAssetInjective } from "./getForeignAsset";
|
||||
|
||||
|
@ -114,3 +115,19 @@ export function getIsWrappedAssetNear(
|
|||
): boolean {
|
||||
return asset.endsWith("." + tokenBridge);
|
||||
}
|
||||
|
||||
// TODO: do we need to check if token is registered in bridge?
|
||||
export async function getIsWrappedAssetAptos(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
assetAddress: string,
|
||||
): Promise<boolean> {
|
||||
assetAddress = ensureHexPrefix(assetAddress);
|
||||
try {
|
||||
// get origin info from asset address
|
||||
await client.getAccountResource(assetAddress, `${tokenBridgeAddress}::state::OriginInfo`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ import { importTokenWasm } from "../solana/wasm";
|
|||
import { buildNativeId } from "../terra";
|
||||
import { canonicalAddress } from "../cosmos";
|
||||
import {
|
||||
assertChain,
|
||||
ChainId,
|
||||
ChainName,
|
||||
CHAIN_ID_ALGORAND,
|
||||
CHAIN_ID_APTOS,
|
||||
CHAIN_ID_NEAR,
|
||||
CHAIN_ID_INJECTIVE,
|
||||
CHAIN_ID_SOLANA,
|
||||
|
@ -25,6 +27,7 @@ import {
|
|||
coalesceCosmWasmChainId,
|
||||
tryHexToNativeAssetString,
|
||||
callFunctionNear,
|
||||
isValidAptosType,
|
||||
} from "../utils";
|
||||
import { safeBigIntToNumber } from "../utils/bigint";
|
||||
import {
|
||||
|
@ -34,6 +37,9 @@ import {
|
|||
} from "./getIsWrappedAsset";
|
||||
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";;
|
||||
|
||||
// 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 {
|
||||
|
@ -284,7 +290,7 @@ export async function getOriginalAssetNear(
|
|||
chainId: CHAIN_ID_NEAR,
|
||||
assetAddress: new Uint8Array(),
|
||||
};
|
||||
retVal.isWrapped = await getIsWrappedAssetNear(tokenAccount, assetAccount);
|
||||
retVal.isWrapped = getIsWrappedAssetNear(tokenAccount, assetAccount);
|
||||
if (!retVal.isWrapped) {
|
||||
retVal.assetAddress = assetAccount
|
||||
? arrayify(sha256(Buffer.from(assetAccount)))
|
||||
|
@ -306,3 +312,50 @@ export async function getOriginalAssetNear(
|
|||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export async function getOriginalAssetAptos(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
fullyQualifiedType: string
|
||||
): Promise<WormholeWrappedInfo> {
|
||||
if (!isValidAptosType(fullyQualifiedType)) {
|
||||
throw new Error("Need fully qualified address");
|
||||
}
|
||||
|
||||
let originInfo: OriginInfo | undefined;
|
||||
try {
|
||||
originInfo = (
|
||||
await client.getAccountResource(
|
||||
fullyQualifiedType.split("::")[0],
|
||||
`${tokenBridgeAddress}::state::OriginInfo`
|
||||
)
|
||||
).data as OriginInfo;
|
||||
} catch {
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_APTOS,
|
||||
assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
|
||||
};
|
||||
}
|
||||
|
||||
if (!!originInfo) {
|
||||
// wrapped asset
|
||||
const chainId = parseInt(originInfo.token_chain.number);
|
||||
assertChain(chainId);
|
||||
const assetAddress = hexToUint8Array(
|
||||
originInfo.token_address.external_address.substring(2)
|
||||
);
|
||||
return {
|
||||
isWrapped: true,
|
||||
chainId,
|
||||
assetAddress,
|
||||
};
|
||||
} else {
|
||||
// native asset
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_APTOS,
|
||||
assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
WSOL_DECIMALS,
|
||||
uint8ArrayToHex,
|
||||
callFunctionNear,
|
||||
hashLookup,
|
||||
hashLookup
|
||||
} from "../utils";
|
||||
|
||||
import { getForeignAssetNear } from ".";
|
||||
|
@ -37,6 +37,8 @@ import { MsgExecuteContract as MsgExecuteContractInjective } from "@injectivelab
|
|||
import { FunctionCallOptions } from "near-api-js/lib/account";
|
||||
import { Provider } from "near-api-js/lib/providers";
|
||||
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
|
||||
import { AptosClient, Types } from "aptos";
|
||||
import { completeTransfer as completeTransferAptos, completeTransferAndRegister } from "../aptos";
|
||||
|
||||
export async function redeemOnEth(
|
||||
tokenBridgeAddress: string,
|
||||
|
@ -382,3 +384,15 @@ export async function redeemOnNear(
|
|||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function redeemOnAptos(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
transferVAA: Uint8Array
|
||||
): Promise<Types.EntryFunctionPayload> {
|
||||
return completeTransferAndRegister(
|
||||
client,
|
||||
tokenBridgeAddress,
|
||||
transferVAA
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,10 +48,12 @@ import {
|
|||
} 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";
|
||||
import { transferTokens as transferTokensAptos } from "../aptos";
|
||||
|
||||
export async function getAllowanceEth(
|
||||
tokenBridgeAddress: string,
|
||||
|
@ -958,3 +960,24 @@ export async function transferNearFromNear(
|
|||
gas: new BN("100000000000000"),
|
||||
};
|
||||
}
|
||||
|
||||
export function transferFromAptos(
|
||||
tokenBridgeAddress: string,
|
||||
fullyQualifiedType: string,
|
||||
amount: string,
|
||||
recipientChain: ChainId | ChainName,
|
||||
recipient: Uint8Array,
|
||||
relayerFee: string = "0",
|
||||
payload: string = ""
|
||||
): Types.EntryFunctionPayload {
|
||||
return transferTokensAptos(
|
||||
tokenBridgeAddress,
|
||||
fullyQualifiedType,
|
||||
amount,
|
||||
recipientChain,
|
||||
recipient,
|
||||
relayerFee,
|
||||
createNonce().readUInt32LE(0),
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
createWrappedOnNear,
|
||||
submitVAAOnInjective,
|
||||
createWrappedOnXpla,
|
||||
createWrappedOnAptos,
|
||||
} from ".";
|
||||
import { Bridge__factory } from "../ethers-contracts";
|
||||
|
||||
|
@ -32,3 +33,5 @@ export const updateWrappedOnSolana = createWrappedOnSolana;
|
|||
export const updateWrappedOnAlgorand = createWrappedOnAlgorand;
|
||||
|
||||
export const updateWrappedOnNear = createWrappedOnNear;
|
||||
|
||||
export const updateWrappedOnAptos = createWrappedOnAptos;
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
import { hexZeroPad } from "ethers/lib/utils";
|
||||
import { sha3_256 } from "js-sha3";
|
||||
import { ChainId, CHAIN_ID_APTOS, ensureHexPrefix, hex } from "../utils";
|
||||
import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos";
|
||||
import { State } from "../aptos/types";
|
||||
|
||||
export const signAndSubmitEntryFunction = (
|
||||
client: AptosClient,
|
||||
sender: AptosAccount,
|
||||
payload: Types.EntryFunctionPayload,
|
||||
opts?: Partial<Types.SubmitTransactionRequest>
|
||||
): Promise<Types.UserTransaction> => {
|
||||
// overwriting `max_gas_amount` and `gas_unit_price` defaults
|
||||
// rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
|
||||
const customOpts = Object.assign(
|
||||
{
|
||||
gas_unit_price: "100",
|
||||
max_gas_amount: "30000",
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
||||
return client
|
||||
.generateTransaction(sender.address(), payload, customOpts)
|
||||
.then(
|
||||
(rawTx) =>
|
||||
signAndSubmitTransaction(
|
||||
client,
|
||||
sender,
|
||||
rawTx
|
||||
) as Promise<Types.UserTransaction>
|
||||
);
|
||||
};
|
||||
|
||||
export const signAndSubmitScript = async (
|
||||
client: AptosClient,
|
||||
sender: AptosAccount,
|
||||
payload: TxnBuilderTypes.TransactionPayloadScript,
|
||||
opts?: Partial<Types.SubmitTransactionRequest>
|
||||
) => {
|
||||
// overwriting `max_gas_amount` and `gas_unit_price` defaults
|
||||
// rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
|
||||
const customOpts = Object.assign(
|
||||
{
|
||||
gas_unit_price: "100",
|
||||
max_gas_amount: "30000",
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
||||
// create raw transaction
|
||||
const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
|
||||
client.getAccount(sender.address()),
|
||||
client.getChainId(),
|
||||
]);
|
||||
const rawTx = new TxnBuilderTypes.RawTransaction(
|
||||
TxnBuilderTypes.AccountAddress.fromHex(sender.address()),
|
||||
BigInt(sequenceNumber),
|
||||
payload,
|
||||
BigInt(customOpts.max_gas_amount),
|
||||
BigInt(customOpts.gas_unit_price),
|
||||
BigInt(Math.floor(Date.now() / 1000) + 10),
|
||||
new TxnBuilderTypes.ChainId(chainId)
|
||||
);
|
||||
// sign & submit transaction
|
||||
return signAndSubmitTransaction(client, sender, rawTx);
|
||||
};
|
||||
|
||||
const signAndSubmitTransaction = async (
|
||||
client: AptosClient,
|
||||
sender: AptosAccount,
|
||||
rawTx: TxnBuilderTypes.RawTransaction
|
||||
): Promise<Types.Transaction> => {
|
||||
// simulate transaction
|
||||
await client.simulateTransaction(sender, rawTx).then((sims) =>
|
||||
sims.forEach((tx) => {
|
||||
if (!tx.success) {
|
||||
throw new Error(
|
||||
`Transaction failed: ${tx.vm_status}\n${JSON.stringify(tx, null, 2)}`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// sign & submit transaction
|
||||
return client
|
||||
.signTransaction(sender, rawTx)
|
||||
.then((signedTx) => client.submitTransaction(signedTx))
|
||||
.then((pendingTx) => client.waitForTransactionWithResult(pendingTx.hash));
|
||||
};
|
||||
|
||||
export const getAssetFullyQualifiedType = (
|
||||
tokenBridgeAddress: string, // 32 bytes
|
||||
originChain: ChainId,
|
||||
originAddress: string
|
||||
): string | null => {
|
||||
// native asset
|
||||
if (originChain === CHAIN_ID_APTOS) {
|
||||
// originAddress should be of form address::module::type
|
||||
if (!isValidAptosType(originAddress)) {
|
||||
console.error("Need fully qualified address for native asset");
|
||||
return null;
|
||||
}
|
||||
|
||||
return ensureHexPrefix(originAddress);
|
||||
}
|
||||
|
||||
// non-native asset, derive unique address
|
||||
const wrappedAssetAddress = getForeignAssetAddress(
|
||||
tokenBridgeAddress,
|
||||
originChain,
|
||||
originAddress
|
||||
);
|
||||
return wrappedAssetAddress
|
||||
? `${ensureHexPrefix(wrappedAssetAddress)}::coin::T`
|
||||
: null;
|
||||
};
|
||||
|
||||
export const getForeignAssetAddress = (
|
||||
tokenBridgeAddress: string, // 32 bytes
|
||||
originChain: ChainId,
|
||||
originAddress: string
|
||||
): string | null => {
|
||||
if (originChain === CHAIN_ID_APTOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// from https://github.com/aptos-labs/aptos-core/blob/25696fd266498d81d346fe86e01c330705a71465/aptos-move/framework/aptos-framework/sources/account.move#L90-L95
|
||||
let DERIVE_RESOURCE_ACCOUNT_SCHEME = Buffer.alloc(1);
|
||||
DERIVE_RESOURCE_ACCOUNT_SCHEME.writeUInt8(255);
|
||||
|
||||
let chain: Buffer = Buffer.alloc(2);
|
||||
chain.writeUInt16BE(originChain);
|
||||
return sha3_256(
|
||||
Buffer.concat([
|
||||
hex(hexZeroPad(ensureHexPrefix(tokenBridgeAddress), 32)),
|
||||
chain,
|
||||
Buffer.from("::", "ascii"),
|
||||
hex(hexZeroPad(ensureHexPrefix(originAddress), 32)),
|
||||
DERIVE_RESOURCE_ACCOUNT_SCHEME,
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
export const isValidAptosType = (address: string): boolean =>
|
||||
/^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(address);
|
||||
|
||||
export const getExternalAddressFromType = (
|
||||
fullyQualifiedType: string
|
||||
): string => {
|
||||
// hash the type so it fits into 32 bytes
|
||||
return sha3_256(fullyQualifiedType);
|
||||
};
|
||||
|
||||
export async function getTypeFromExternalAddress(
|
||||
client: AptosClient,
|
||||
tokenBridgeAddress: string,
|
||||
fullyQualifiedTypeHash: string
|
||||
): Promise<string | null> {
|
||||
// get handle
|
||||
tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
|
||||
const state = (
|
||||
await client.getAccountResource(
|
||||
tokenBridgeAddress,
|
||||
`${tokenBridgeAddress}::state::State`
|
||||
)
|
||||
).data as State;
|
||||
const handle = state.native_infos.handle;
|
||||
|
||||
try {
|
||||
// get type info
|
||||
const typeInfo = await client.getTableItem(handle, {
|
||||
key_type: `${tokenBridgeAddress}::token_hash::TokenHash`,
|
||||
value_type: "0x1::type_info::TypeInfo",
|
||||
key: { hash: fullyQualifiedTypeHash },
|
||||
});
|
||||
|
||||
if (!typeInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// construct type
|
||||
const moduleName = Buffer.from(
|
||||
typeInfo.module_name.substring(2),
|
||||
"hex"
|
||||
).toString("ascii");
|
||||
const structName = Buffer.from(
|
||||
typeInfo.struct_name.substring(2),
|
||||
"hex"
|
||||
).toString("ascii");
|
||||
return `${typeInfo.account_address}::${moduleName}::${structName}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { arrayify, zeroPad } from "@ethersproject/bytes";
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import { hexValue, hexZeroPad, sha256, stripZeros } from "ethers/lib/utils";
|
||||
import { Provider as NearProvider } from "near-api-js/lib/providers";
|
||||
import { ethers } from "ethers";
|
||||
import {
|
||||
hexToNativeAssetStringAlgorand,
|
||||
nativeStringToHexAlgorand,
|
||||
|
@ -14,12 +15,13 @@ import {
|
|||
ChainId,
|
||||
ChainName,
|
||||
CHAIN_ID_ALGORAND,
|
||||
CHAIN_ID_NEAR,
|
||||
CHAIN_ID_INJECTIVE,
|
||||
CHAIN_ID_OSMOSIS,
|
||||
CHAIN_ID_SUI,
|
||||
CHAIN_ID_APTOS,
|
||||
CHAIN_ID_INJECTIVE,
|
||||
CHAIN_ID_NEAR,
|
||||
CHAIN_ID_OSMOSIS,
|
||||
CHAIN_ID_PYTHNET,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_SUI,
|
||||
CHAIN_ID_TERRA,
|
||||
CHAIN_ID_TERRA2,
|
||||
CHAIN_ID_WORMCHAIN,
|
||||
|
@ -27,10 +29,10 @@ import {
|
|||
coalesceChainId,
|
||||
isEVMChain,
|
||||
isTerraChain,
|
||||
CHAIN_ID_PYTHNET,
|
||||
CHAIN_ID_XPLA,
|
||||
} from "./consts";
|
||||
import { hashLookup } from "./near";
|
||||
import { getExternalAddressFromType, isValidAptosType } from "./aptos";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -241,7 +243,11 @@ export const tryNativeToHexString = (
|
|||
} else if (chainId === CHAIN_ID_SUI) {
|
||||
throw Error("hexToNativeString: Sui not supported yet.");
|
||||
} else if (chainId === CHAIN_ID_APTOS) {
|
||||
throw Error("hexToNativeString: Aptos not supported yet.");
|
||||
if (isValidAptosType(address)) {
|
||||
return getExternalAddressFromType(address);
|
||||
}
|
||||
|
||||
return uint8ArrayToHex(zeroPad(arrayify(address, { allowMissingPrefix:true }), 32));
|
||||
} else if (chainId === CHAIN_ID_UNSET) {
|
||||
throw Error("hexToNativeString: Chain id unset");
|
||||
} else {
|
||||
|
@ -309,3 +315,14 @@ export function textToHexString(name: string): string {
|
|||
export function textToUint8Array(name: string): Uint8Array {
|
||||
return new Uint8Array(Buffer.from(name, "binary"));
|
||||
}
|
||||
|
||||
export function hex(x: string): Buffer {
|
||||
return Buffer.from(
|
||||
ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2),
|
||||
"hex"
|
||||
);
|
||||
}
|
||||
|
||||
export function ensureHexPrefix(x: string): string {
|
||||
return x.substring(0, 2) !== "0x" ? `0x${x}` : x;
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ export type EVMChainName =
|
|||
| "optimism"
|
||||
| "gnosis"
|
||||
| "ropsten";
|
||||
|
||||
/**
|
||||
*
|
||||
* All the Solana-based chain names that Wormhole supports
|
||||
|
@ -75,6 +76,8 @@ export type ChainContracts = {
|
|||
[chain in ChainName]: Contracts;
|
||||
};
|
||||
|
||||
export type Network = "MAINNET" | "TESTNET" | "DEVNET";
|
||||
|
||||
const MAINNET = {
|
||||
unset: {
|
||||
core: undefined,
|
||||
|
@ -167,8 +170,9 @@ const MAINNET = {
|
|||
nft_bridge: undefined,
|
||||
},
|
||||
aptos: {
|
||||
core: undefined,
|
||||
token_bridge: undefined,
|
||||
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
|
||||
token_bridge:
|
||||
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
|
||||
nft_bridge: undefined,
|
||||
},
|
||||
sui: {
|
||||
|
@ -214,7 +218,8 @@ const MAINNET = {
|
|||
},
|
||||
xpla: {
|
||||
core: "xpla1jn8qmdda5m6f6fqu9qv46rt7ajhklg40ukpqchkejcvy8x7w26cqxamv3w",
|
||||
token_bridge: "xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
|
||||
token_bridge:
|
||||
"xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
|
||||
nft_bridge: undefined,
|
||||
},
|
||||
ropsten: {
|
||||
|
@ -321,8 +326,9 @@ const TESTNET = {
|
|||
nft_bridge: undefined,
|
||||
},
|
||||
aptos: {
|
||||
core: undefined,
|
||||
token_bridge: undefined,
|
||||
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
|
||||
token_bridge:
|
||||
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
|
||||
nft_bridge: undefined,
|
||||
},
|
||||
sui: {
|
||||
|
@ -476,8 +482,9 @@ const DEVNET = {
|
|||
nft_bridge: undefined,
|
||||
},
|
||||
aptos: {
|
||||
core: undefined,
|
||||
token_bridge: undefined,
|
||||
core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017",
|
||||
token_bridge:
|
||||
"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31",
|
||||
nft_bridge: undefined,
|
||||
},
|
||||
sui: {
|
||||
|
@ -781,6 +788,8 @@ export function assertEVMChain(
|
|||
export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112";
|
||||
export const WSOL_DECIMALS = 9;
|
||||
export const MAX_VAA_DECIMALS = 8;
|
||||
export const APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS =
|
||||
"0000000000000000000000000000000000000000000000000000000000000001";
|
||||
|
||||
export const TERRA_REDEEMED_CHECK_WALLET_ADDRESS =
|
||||
"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from "./consts";
|
||||
export * from "./createNonce";
|
||||
export * from "./parseVaa";
|
||||
export * from "./aptos";
|
||||
export * from "./array";
|
||||
export * from "./bigint";
|
||||
export * from "./consts";
|
||||
export * from "./createNonce";
|
||||
export * from "./near";
|
||||
export * from "./parseVaa";
|
||||
|
|
Loading…
Reference in New Issue