Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solana SDK support #690

Closed
dev-johnny-gh opened this issue Sep 4, 2023 · 2 comments
Closed

Solana SDK support #690

dev-johnny-gh opened this issue Sep 4, 2023 · 2 comments

Comments

@dev-johnny-gh
Copy link

dev-johnny-gh commented Sep 4, 2023

Our user submitted a support ticket and said he failed to redeem the TBTC on Solana. So, I noticed you guys added native TBTC support on Solana. Then, I tried to integrate it into our site. But I failed.

I can't find the SDK in this repo, so I created one, but it won't work. Then I checked all the accounts on the ix, and they're fine. Therefore, I suspect something is wrong with the program or its coder.

Can someone tell me how to construct the program properly on the browser without the deployment data?

I moved some code from your tests to my code, and you can search new Program to see how I constructed the program.

import {
  Commitment,
  Connection,
  Keypair,
  PublicKey,
  SYSVAR_CLOCK_PUBKEY,
  SYSVAR_RENT_PUBKEY,
  Transaction,
  TransactionInstruction,
} from '@solana/web3.js';
import * as coreBridge from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole';
import * as tokenBridge from '@certusone/wormhole-sdk/lib/cjs/solana/tokenBridge';
import { CORE_BRIDGE_DATA, TBTC_PROGRAM_ID, WRAPPED_TBTC_ASSET } from '../consts';
import { BN, Program } from '@coral-xyz/anchor';
import { WormholeGateway } from './types/tbtc-wormhole-gateway';
import WormholeGatewayIDL from './idls/tbtc-wormhole-gateway.json';
import { CarrierChainId } from '../../../../utils/consts';
import {
  CHAIN_ID_SOLANA,
  ParsedVaa,
  SignedVaa,
  createNonce,
  parseTokenTransferPayload,
  parseVaa,
} from '@certusone/wormhole-sdk';
import { tryCarrierUint8ArrayToNative } from '../../../../utils/web3Utils';
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { getTBTCAddressForChain, getTBTCGatewayForChain, getWtBTCAddressForChain } from '../../../../utils/tbtc';
import { errorGettingAccountPubKey, errorGettingSplTokenPubKey } from './error';
import { SystemProgram } from '@solana/web3.js';

function getCustodianPDA(gatewayProgram: PublicKey): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from('redeemer')], gatewayProgram)[0];
}

function getGatewayInfoPDA(targetChain: number, gatewayProgram: PublicKey): PublicKey {
  const encodedChain = Buffer.alloc(2);
  encodedChain.writeUInt16LE(targetChain);
  return PublicKey.findProgramAddressSync([Buffer.from('gateway-info'), encodedChain], gatewayProgram)[0];
}

function getTokenBridgeCoreEmitter(tokenBridgeProgram: PublicKey) {
  const [tokenBridgeCoreEmitter] = PublicKey.findProgramAddressSync([Buffer.from('emitter')], tokenBridgeProgram);

  return tokenBridgeCoreEmitter;
}

function getConfigPDA(tbtcProgram: PublicKey): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from('config')], tbtcProgram)[0];
}

function getMinterInfoPDA(minter: PublicKey, tbtcProgram: PublicKey): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from('minter-info'), minter.toBuffer()], tbtcProgram)[0];
}

function getWrappedTbtcTokenPDA(gatewayProgram: PublicKey): PublicKey {
  return PublicKey.findProgramAddressSync([Buffer.from('wrapped-token')], gatewayProgram)[0];
}

type SendTbtcGatewayContext = {
  senderToken: PublicKey;
  sender: PublicKey;
  tokenBridgeProgram: PublicKey;
  coreBridgeProgram: PublicKey;
  gatewayProgram: PublicKey;
  tbtcMint: PublicKey;
  wrappedTbtcMint: PublicKey;

  custodian?: PublicKey;
  gatewayInfo?: PublicKey;
  tokenBridgeConfig?: PublicKey;
  tokenBridgeWrappedAsset?: PublicKey;
  tokenBridgeTransferAuthority?: PublicKey;
  coreBridgeData?: PublicKey;
  tokenBridgeCoreEmitter?: PublicKey;
  coreEmitterSequence?: PublicKey;
  coreFeeCollector?: PublicKey;
  clock?: PublicKey;
  tokenBridgeSender?: PublicKey;
  wrappedTbtcToken?: PublicKey;
  rent?: PublicKey;
};

type SendTbtcGatewayArgs = {
  amount: BN;
  recipientChain: number;
  recipient: number[];
  nonce: number;
};

async function sendTbtcGatewayIx(
  accounts: SendTbtcGatewayContext,
  args: SendTbtcGatewayArgs,
  message: Keypair,
): Promise<TransactionInstruction> {
  let {
    custodian,
    gatewayInfo,
    wrappedTbtcMint,
    tbtcMint,
    senderToken,
    sender,
    tokenBridgeProgram,
    coreBridgeProgram,
    gatewayProgram,
    tokenBridgeConfig,
    tokenBridgeWrappedAsset,
    tokenBridgeTransferAuthority,
    coreBridgeData,
    tokenBridgeCoreEmitter,
    coreEmitterSequence,
    coreFeeCollector,
    clock,
    tokenBridgeSender,
    wrappedTbtcToken,
    rent,
  } = accounts;

  const program = new Program(WormholeGatewayIDL as WormholeGateway, gatewayProgram, {
    connection: null,
  } as any);

  if (custodian === undefined) {
    custodian = getCustodianPDA(gatewayProgram);
  }

  if (gatewayInfo === undefined) {
    gatewayInfo = getGatewayInfoPDA(args.recipientChain, gatewayProgram);
  }

  if (wrappedTbtcToken === undefined) {
    wrappedTbtcToken = getWrappedTbtcTokenPDA(gatewayProgram);
  }

  if (tokenBridgeConfig === undefined) {
    tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey(tokenBridgeProgram);
  }

  if (tokenBridgeWrappedAsset === undefined) {
    tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET;
  }

  if (tokenBridgeTransferAuthority === undefined) {
    tokenBridgeTransferAuthority = tokenBridge.deriveAuthoritySignerKey(tokenBridgeProgram);
  }

  if (coreBridgeData === undefined) {
    coreBridgeData = CORE_BRIDGE_DATA;
  }

  if (tokenBridgeCoreEmitter === undefined) {
    tokenBridgeCoreEmitter = getTokenBridgeCoreEmitter(tokenBridgeProgram);
  }

  if (coreEmitterSequence === undefined) {
    coreEmitterSequence = coreBridge.deriveEmitterSequenceKey(tokenBridgeCoreEmitter, coreBridgeProgram);
  }

  if (coreFeeCollector === undefined) {
    coreFeeCollector = coreBridge.deriveFeeCollectorKey(coreBridgeProgram);
  }

  if (clock === undefined) {
    clock = SYSVAR_CLOCK_PUBKEY;
  }

  if (tokenBridgeSender === undefined) {
    tokenBridgeSender = tokenBridge.deriveSenderAccountKey(gatewayProgram);
  }

  if (rent === undefined) {
    rent = SYSVAR_RENT_PUBKEY;
  }

  console.log(
    'sendTbtc',
    JSON.stringify({
      custodian,
      gatewayInfo,
      wrappedTbtcToken,
      wrappedTbtcMint,
      tbtcMint,
      senderToken,
      sender,
      tokenBridgeConfig,
      tokenBridgeWrappedAsset,
      tokenBridgeTransferAuthority,
      coreBridgeData,
      coreMessage: message.publicKey,
      tokenBridgeCoreEmitter,
      coreEmitterSequence,
      coreFeeCollector,
      clock,
      tokenBridgeSender,
      rent,
      tokenBridgeProgram,
      coreBridgeProgram,
    }),
  );

  return program.methods
    .sendTbtcGateway(args)
    .accounts({
      custodian,
      gatewayInfo,
      wrappedTbtcToken,
      wrappedTbtcMint,
      tbtcMint,
      senderToken,
      sender,
      tokenBridgeConfig,
      tokenBridgeWrappedAsset,
      tokenBridgeTransferAuthority,
      coreBridgeData,
      coreMessage: message.publicKey,
      tokenBridgeCoreEmitter,
      coreEmitterSequence,
      coreFeeCollector,
      clock,
      tokenBridgeSender,
      rent,
      tokenBridgeProgram,
      coreBridgeProgram,
    })
    .instruction();
}

export async function transferTbtcFromSolana(options: {
  connection: Connection;
  bridgeAddress: PublicKey;
  tokenBridgeAddress: PublicKey;
  payerAddress: PublicKey;
  fromAddress: PublicKey;
  mintAddress: PublicKey;
  wrappedTbtcMint: PublicKey;
  amount: bigint;
  targetAddress: Uint8Array | Buffer;
  targetChain: CarrierChainId;
  commitment?: Commitment;
}) {
  const {
    connection,
    bridgeAddress,
    tokenBridgeAddress,
    payerAddress,
    fromAddress,
    mintAddress,
    wrappedTbtcMint,
    amount,
    targetAddress,
    targetChain,
    commitment,
  } = options;

  const nonce = createNonce().readUInt32LE(0);
  const message = Keypair.generate();
  const gatewayProgram = new PublicKey(getTBTCGatewayForChain(CHAIN_ID_SOLANA));
  const sendTbtcIx = await sendTbtcGatewayIx(
    {
      sender: payerAddress,
      senderToken: fromAddress,
      tokenBridgeProgram: tokenBridgeAddress,
      coreBridgeProgram: bridgeAddress,
      gatewayProgram,
      tbtcMint: mintAddress,
      wrappedTbtcMint,
    },
    {
      amount: new BN(amount.toString()),
      recipientChain: targetChain,
      recipient: Array.from(targetAddress),
      nonce,
    },
    message,
  );
  const transaction = new Transaction().add(sendTbtcIx);
  const { blockhash } = await connection.getLatestBlockhash(commitment);
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = new PublicKey(payerAddress);
  transaction.partialSign(message);
  return transaction;
}

type ReceiveTbtcContext = {
  payer: PublicKey;
  recipient: PublicKey;
  recipientToken: PublicKey;
  recipientWrappedToken: PublicKey;
  tokenBridgeProgram: PublicKey;
  coreBridgeProgram: PublicKey;
  gatewayProgram: PublicKey;
  tbtcMint: PublicKey;
  wrappedTbtcMint: PublicKey;

  custodian?: PublicKey;
  postedVaa?: PublicKey;
  tokenBridgeClaim?: PublicKey;
  tbtcConfig?: PublicKey;
  tbtcMinterInfo?: PublicKey;
  tokenBridgeConfig?: PublicKey;
  tokenBridgeRegisteredEmitter?: PublicKey;
  tokenBridgeWrappedAsset?: PublicKey;
  tokenBridgeMintAuthority?: PublicKey;
  rent?: PublicKey;
  tbtcProgram?: PublicKey;
  wrappedTbtcToken?: PublicKey;
};

export async function receiveTbtcIx(
  accounts: ReceiveTbtcContext,
  parsedVaa: ParsedVaa,
): Promise<TransactionInstruction> {
  let {
    payer,
    recipient,
    recipientToken,
    recipientWrappedToken,
    tokenBridgeProgram,
    coreBridgeProgram,
    gatewayProgram,
    tbtcMint,
    wrappedTbtcMint,

    custodian,
    postedVaa,
    tokenBridgeClaim,
    tbtcConfig,
    tbtcMinterInfo,
    tokenBridgeConfig,
    tokenBridgeRegisteredEmitter,
    tokenBridgeWrappedAsset,
    tokenBridgeMintAuthority,
    rent,
    tbtcProgram,
    wrappedTbtcToken,
  } = accounts;

  const program = new Program(WormholeGatewayIDL as WormholeGateway, new PublicKey(gatewayProgram), {
    connection: null,
  } as any);

  if (custodian === undefined) {
    custodian = getCustodianPDA(gatewayProgram);
  }

  if (postedVaa === undefined) {
    postedVaa = coreBridge.derivePostedVaaKey(coreBridgeProgram, parsedVaa.hash);
  }

  if (tokenBridgeClaim === undefined) {
    tokenBridgeClaim = coreBridge.deriveClaimKey(
      tokenBridgeProgram,
      parsedVaa.emitterAddress,
      parsedVaa.emitterChain,
      parsedVaa.sequence,
    );
  }

  if (wrappedTbtcToken === undefined) {
    wrappedTbtcToken = getWrappedTbtcTokenPDA(gatewayProgram);
  }

  if (tokenBridgeRegisteredEmitter === undefined) {
    tokenBridgeRegisteredEmitter = tokenBridge.deriveEndpointKey(
      tokenBridgeProgram,
      parsedVaa.emitterChain,
      parsedVaa.emitterAddress,
    );
  }

  if (tbtcProgram === undefined) {
    tbtcProgram = TBTC_PROGRAM_ID;
  }

  if (tbtcConfig === undefined) {
    tbtcConfig = getConfigPDA(tbtcProgram);
  }

  if (tbtcMinterInfo === undefined) {
    tbtcMinterInfo = getMinterInfoPDA(custodian, tbtcProgram);
  }

  if (tokenBridgeConfig === undefined) {
    tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey(tokenBridgeProgram);
  }

  if (tokenBridgeWrappedAsset === undefined) {
    tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET;
  }

  if (tokenBridgeMintAuthority === undefined) {
    tokenBridgeMintAuthority = tokenBridge.deriveMintAuthorityKey(tokenBridgeProgram);
  }

  if (rent === undefined) {
    rent = SYSVAR_RENT_PUBKEY;
  }

  console.log(
    'receiveTbtc',
    JSON.stringify({
      payer,
      custodian,
      postedVaa,
      tokenBridgeClaim,
      wrappedTbtcToken,
      wrappedTbtcMint,
      tbtcMint,
      recipientToken,
      recipient,
      recipientWrappedToken,
      tbtcConfig,
      tbtcMinterInfo,
      tokenBridgeConfig,
      tokenBridgeRegisteredEmitter,
      tokenBridgeWrappedAsset,
      tokenBridgeMintAuthority,
      rent,
      tbtcProgram,
      tokenBridgeProgram,
      coreBridgeProgram,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      tokenProgram: TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
    }),
  );

  return program.methods
    .receiveTbtc(Array.from(parsedVaa.hash))
    .accounts({
      payer,
      custodian,
      postedVaa,
      tokenBridgeClaim,
      wrappedTbtcToken,
      wrappedTbtcMint,
      tbtcMint,
      recipientToken,
      recipient,
      recipientWrappedToken,
      tbtcConfig,
      tbtcMinterInfo,
      tokenBridgeConfig,
      tokenBridgeRegisteredEmitter,
      tokenBridgeWrappedAsset,
      tokenBridgeMintAuthority,
      rent,
      tbtcProgram,
      tokenBridgeProgram,
      coreBridgeProgram,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      tokenProgram: TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
    })
    .instruction();
}

export async function redeemTbtcOnSolana(options: {
  connection: Connection;
  bridgeAddress: PublicKey;
  tokenBridgeAddress: PublicKey;
  payerAddress: PublicKey;
  signedVAA: SignedVaa;
  commitment?: Commitment;
}) {
  const { connection, bridgeAddress, tokenBridgeAddress, payerAddress, signedVAA, commitment } = options;

  const parsedVaa = parseVaa(signedVAA);
  const tokenTransferPayload = parseTokenTransferPayload(parsedVaa.payload);
  const recipientAssociatedAccountUnit8Array = tokenTransferPayload.tokenTransferPayload;
  const recipientAssociatedAccount = tryCarrierUint8ArrayToNative(
    recipientAssociatedAccountUnit8Array,
    tokenTransferPayload.toChain as CarrierChainId,
  );

  const recipientAssociatedAccountPubkey = new PublicKey(recipientAssociatedAccount);
  const recipientAssociatedAccountInfo = await connection.getParsedAccountInfo(recipientAssociatedAccountPubkey);
  const recipient =
    recipientAssociatedAccountInfo &&
    recipientAssociatedAccountInfo.value &&
    'parsed' in recipientAssociatedAccountInfo.value.data &&
    recipientAssociatedAccountInfo.value.data.parsed.info.owner;

  if (!recipient) {
    throw errorGettingAccountPubKey;
  }

  const recipientPubkey = new PublicKey(recipient);
  const splParsedTokenAccounts = await connection.getParsedTokenAccountsByOwner(recipientPubkey, {
    programId: new PublicKey(TOKEN_PROGRAM_ID),
  });
  const solTbtcMint = getTBTCAddressForChain(CHAIN_ID_SOLANA);
  const solWrappedTbtcMint = getWtBTCAddressForChain(CHAIN_ID_SOLANA);
  const solTbtcAccount = splParsedTokenAccounts.value.find(
    (item) => item.account.data.parsed?.info?.mint?.toString() === solTbtcMint,
  );
  const solWrappedTbtcAccount = splParsedTokenAccounts.value.find(
    (item) => item.account.data.parsed?.info?.mint?.toString() === solWrappedTbtcMint,
  );

  console.log('solTbtcAccount', solTbtcAccount);

  if (!solTbtcAccount) {
    throw errorGettingSplTokenPubKey;
  }
  console.log('solWrappedTbtcAccount', solWrappedTbtcAccount);

  if (!solWrappedTbtcAccount) {
    throw errorGettingSplTokenPubKey;
  }

  const gatewayProgram = new PublicKey(getTBTCGatewayForChain(CHAIN_ID_SOLANA));
  const redeemTbtcIx = await receiveTbtcIx(
    {
      recipient: recipientPubkey,
      recipientToken: solTbtcAccount.pubkey,
      recipientWrappedToken: solWrappedTbtcAccount.pubkey,
      payer: payerAddress,
      coreBridgeProgram: bridgeAddress,
      tokenBridgeProgram: tokenBridgeAddress,
      gatewayProgram,
      tbtcMint: new PublicKey(solTbtcMint),
      wrappedTbtcMint: new PublicKey(solWrappedTbtcMint),
    },
    parsedVaa,
  );
  const transaction = new Transaction().add(redeemTbtcIx);
  const { blockhash } = await connection.getLatestBlockhash(commitment);
  
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = new PublicKey(payerAddress);

  return transaction;
}

Here is the accounts output from my code. (mainnet data):

{
  "payer": "CwZXBCp7BEDSgRKsJ68D46guK1MUu9H4npd2GZTNcZMn",
  "custodian": "7E4rfPC38Qf1Q1596DEtfe731BfK69GzFj9FkgLKECbB",
  "postedVaa": "6ho2tnfbZMXv44pfYuiWAortibPa9zEvPwky8pEUi9VA",
  "tokenBridgeClaim": "9eYKeqq67SGtm2Ueue59HbWVuS4j1v4xMd6XNDYmmCzY",
  "wrappedTbtcToken": "EEUs6TPA36riNwBuch8AfZFA4DgADCLQryqCaesEfUUo",
  "wrappedTbtcMint": "25rXTx9zDZcHyTav5sRqM6YBvTGu9pPH9yv83uAEqbgG",
  "tbtcMint": "6DNSN2BJsaPFdFFc1zP37kkeNe4Usc1Sqkzr9C9vPWcU",
  "recipientToken": "3CgNApNtvhBJwW7Z8xmHcMCsdS3esASD6JT8gTaUbW3j",
  "recipient": "CwZXBCp7BEDSgRKsJ68D46guK1MUu9H4npd2GZTNcZMn",
  "recipientWrappedToken": "9WDcdkJthBga9HWFJazZzopuvdAvcTTacJqGRZrxL3C4",
  "tbtcConfig": "2PfR3xUdMS8xHUhE9H47dy2Ro3Ww9agP5GdnNoKj3Vtf",
  "tbtcMinterInfo": "65JpE6JWXm7CCUHHf7raHyk57RgQGMx1PPo1qgi63fh4",
  "tokenBridgeConfig": "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx",
  "tokenBridgeRegisteredEmitter": "12Uj9xTN8paxZtt2kM1eUNmS2JmzAQS2TXFS42KU4Ds6",
  "tokenBridgeWrappedAsset": "5LEUZpBxUQmoxoNGqmYmFEGAPDuhWbAY5CGt519UixLo",
  "tokenBridgeMintAuthority": "BCD75RNBHrJJpW4dXVagL5mPjzRLnVZq4YirJdjEYMV7",
  "rent": "SysvarRent111111111111111111111111111111111",
  "tbtcProgram": "Gj93RRt6QB7FjmyokAD5rcMAku7pq3Fk2Aa8y6nNbwsV",
  "tokenBridgeProgram": "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb",
  "coreBridgeProgram": "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
  "associatedTokenProgram": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
  "tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
  "systemProgram": "11111111111111111111111111111111"
}
@dev-johnny-gh
Copy link
Author

I see your checklist #675, so I guess you can help me with this. @dimpar

@dev-johnny-gh
Copy link
Author

I figured out why my code failed. All accounts are correct, but the recipient on the token transfer payload is incorrect, it's the root cause. the recipient needs to be the Solana wallet address, not the TBTC associated account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant