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 <a5-pickle@users.noreply.github.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
This commit is contained in:
A5 Pickle 2022-10-26 09:28:46 -05:00 committed by GitHub
parent 043d4511a1
commit e109024e99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 17786 additions and 1589 deletions

6
sdk/js/.gitignore vendored
View File

@ -30,4 +30,8 @@ yarn-error.log*
/src/proto
# build
/lib
/lib
# solana idl
/src/anchor-idl
/src/solana/types

View File

@ -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

705
sdk/js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -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();

View File

@ -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", () => {

View File

@ -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
);
}

View File

@ -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) {

View File

@ -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")}`;
}

View File

@ -1,4 +1,3 @@
require("dotenv").config({ path: ".env" });
import { getNetworkInfo, Network } from "@injectivelabs/networks";
import {
ChainGrpcWasmApi,

View File

@ -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";

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
);
}
}

5
sdk/js/src/mock/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from "./governance";
export * from "./misc";
export * from "./nftBridge";
export * from "./tokenBridge";
export * from "./wormhole";

17
sdk/js/src/mock/misc.ts Normal file
View File

@ -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 });
}

View File

@ -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);
}
}

View File

@ -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
);
}
}

129
sdk/js/src/mock/wormhole.ts Normal file
View File

@ -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);
}
}

View File

@ -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🎸");

View File

@ -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<string | null> {
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<string> {
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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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<WormholeWrappedNFTInfo> {
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<WormholeWrappedNFTInfo> {
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);

View File

@ -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<string> } = {}
): Promise<ethers.ContractReceipt> {
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<boolean> {
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<Transaction> {
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<Transaction> {
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<MsgExecuteContract> {
return new MsgExecuteContract(walletAddress, tokenBridgeAddress, {
return new MsgExecuteContract(walletAddress, nftBridgeAddress, {
submit_vaa: {
data: fromUint8Array(signedVAA),
},

View File

@ -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<string> } = {}
): Promise<ethers.ContractReceipt> {
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<Transaction> {
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<MsgExecuteContract[]> {
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,

View File

@ -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)}`);
}
}

View File

@ -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";
}
}

View File

@ -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<IdlField>;
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<PublicKey> {
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<IdlProgramAccount> = 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);
}

View File

@ -0,0 +1,3 @@
export * from "./common";
export * from "./error";
export * from "./idl";

View File

@ -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;
}

View File

@ -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";

View File

@ -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<NftBridgeConfig> {
return getTokenBridgeConfig(connection, nftBridgeProgramId, commitment);
}
export class NftBridgeConfig extends TokenBridgeConfig {}

View File

@ -0,0 +1,13 @@
export * from "./config";
export * from "./wrapped";
export {
EndpointRegistration,
deriveAuthoritySignerKey,
deriveCustodyKey,
deriveCustodySignerKey,
deriveEndpointKey,
deriveMintAuthorityKey,
deriveUpgradeAuthorityKey,
getEndpointRegistration,
} from "../../tokenBridge";

View File

@ -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<WrappedMeta> {
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);
}
}

View File

@ -0,0 +1,40 @@
import { AccountsCoder, Idl } from "@project-serum/anchor";
import { accountSize, IdlTypeDef } from "../../anchor";
export class NftBridgeAccountsCoder<A extends string = string>
implements AccountsCoder
{
constructor(private idl: Idl) {}
public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
switch (accountName) {
default: {
throw new Error(`Invalid account name: ${accountName}`);
}
}
}
public decode<T = any>(accountName: A, ix: Buffer): T {
return this.decodeUnchecked(accountName, ix);
}
public decodeUnchecked<T = any>(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;
}
}

View File

@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("NFT Bridge program does not have events");
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -0,0 +1,12 @@
import { Idl, StateCoder } from "@project-serum/anchor";
export class NftBridgeStateCoder implements StateCoder {
constructor(_idl: Idl) {}
encode<T = any>(_name: string, _account: T): Promise<Buffer> {
throw new Error("NFT Bridge program does not have state");
}
decode<T = any>(_ix: Buffer): T {
throw new Error("NFT Bridge program does not have state");
}
}

View File

@ -0,0 +1,12 @@
import { Idl, TypesCoder } from "@project-serum/anchor";
export class NftBridgeTypesCoder implements TypesCoder {
constructor(_idl: Idl) {}
encode<T = any>(_name: string, _type: T): Buffer {
throw new Error("NFT Bridge program does not have user-defined types");
}
decode<T = any>(_name: string, _typeData: Buffer): T {
throw new Error("NFT Bridge program does not have user-defined types");
}
}

View File

@ -0,0 +1,3 @@
export * from "./accounts";
export * from "./instructions";
export * from "./program";

View File

@ -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
);
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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,
};
}

View File

@ -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";

View File

@ -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,
};
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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<NftBridge> {
return new Program<NftBridge>(
IDL as NftBridge,
new PublicKey(programId),
provider === undefined ? ({ connection: null } as any) : provider,
coder()
);
}
export function createReadOnlyNftBridgeProgramInterface(
programId: PublicKeyInitData,
connection?: Connection
): Program<NftBridge> {
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());
}

View File

@ -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<Transaction>,
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<TransactionInstruction[]> {
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<TransactionInstruction> = 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<TransactionInstruction> {
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<Transaction>,
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<TransactionInstruction> = 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);
}

View File

@ -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

View File

@ -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<TransactionSignatureAndResponse[]> {
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<TransactionSignatureAndResponse[]> {
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<PreparedTransactions> {
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],
};
}

View File

@ -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<TokenBridgeConfig> {
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);
}
}

View File

@ -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);
}

View File

@ -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<EndpointRegistration> {
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);
}
}

View File

@ -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";

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<WrappedMeta> {
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);
}
}

View File

@ -0,0 +1,40 @@
import { AccountsCoder, Idl } from "@project-serum/anchor";
import { accountSize, IdlTypeDef } from "../../anchor";
export class TokenBridgeAccountsCoder<A extends string = string>
implements AccountsCoder
{
constructor(private idl: Idl) {}
public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
switch (accountName) {
default: {
throw new Error(`Invalid account name: ${accountName}`);
}
}
}
public decode<T = any>(accountName: A, ix: Buffer): T {
return this.decodeUnchecked(accountName, ix);
}
public decodeUnchecked<T = any>(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;
}
}

View File

@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("Token Bridge program does not have events");
}
}

View File

@ -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);
}
}

View File

@ -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,
})
);
}

View File

@ -0,0 +1,12 @@
import { Idl, StateCoder } from "@project-serum/anchor";
export class TokenBridgeStateCoder implements StateCoder {
constructor(_idl: Idl) {}
encode<T = any>(_name: string, _account: T): Promise<Buffer> {
throw new Error("Token Bridge program does not have state");
}
decode<T = any>(_ix: Buffer): T {
throw new Error("Token Bridge program does not have state");
}
}

View File

@ -0,0 +1,12 @@
import { Idl, TypesCoder } from "@project-serum/anchor";
export class TokenBridgeTypesCoder implements TypesCoder {
constructor(_idl: Idl) {}
encode<T = any>(_name: string, _type: T): Buffer {
throw new Error("Token Bridge program does not have user-defined types");
}
decode<T = any>(_name: string, _typeData: Buffer): T {
throw new Error("Token Bridge program does not have user-defined types");
}
}

View File

@ -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),
};
}

View File

@ -0,0 +1,4 @@
export * from "./accounts";
export * from "./cpi";
export * from "./instructions";
export * from "./program";

View File

@ -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
);
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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,
};
}

View File

@ -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";

View File

@ -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,
};
}

View File

@ -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),
};
}

View File

@ -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),
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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<TokenBridge> {
return new Program<TokenBridge>(
IDL as TokenBridge,
new PublicKey(programId),
provider === undefined ? ({ connection: null } as any) : provider,
coder()
);
}
export function createReadOnlyTokenBridgeProgramInterface(
programId: PublicKeyInitData,
connection?: Connection
): Program<TokenBridge> {
return createTokenBridgeProgramInterface(
programId,
createReadOnlyProvider(connection)
);
}
export function coder(): TokenBridgeCoder {
return new TokenBridgeCoder(IDL as TokenBridge);
}

View File

@ -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<Buffer>} info - Solana AccountInfo
* @returns serialized data as Buffer
*/
export function getAccountData(info: AccountInfo<Buffer> | null): Buffer {
if (info === null) {
throw Error("account info is null");
}
return info.data;
}

View File

@ -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
);
}

View File

@ -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 };
}

View File

@ -0,0 +1,6 @@
export * from "./account";
export * from "./bpfLoaderUpgradeable";
export * from "./connection";
export * from "./secp256k1";
export * from "./splMetadata";
export * from "./transaction";

View File

@ -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;
}
}

View File

@ -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<Metadata> {
return connection
.getAccountInfo(deriveSplTokenMetadataKey(mint), commitment)
.then((info) => Metadata.deserialize(getAccountData(info)));
}

View File

@ -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<SignatureResult>;
}
/**
* Resembles WalletContextState and Anchor's NodeWallet's signTransaction function signature
*/
export type SignTransaction = (
transaction: Transaction
) => Promise<Transaction>;
/**
*
* @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<TransactionSignatureAndResponse> {
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<TransactionSignatureAndResponse[]> {
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);
}

View File

@ -1 +0,0 @@
export * from "@certusone/wormhole-sdk-wasm";

Some files were not shown because too many files have changed in this diff Show More