Migrate `SealevelTokenAdapter` from warp-routes-UI (#2684)
### Description - Migrating `SealevelTokenAdapter` to monorepo for providing a common abstraction - Common serialization code under `sdk/sealevel` ### Related issues - #2652 ### Backward compatibility Yes ### Testing Manual --------- Co-authored-by: J M Rossy <jm.rossy@gmail.com>pull/2704/head
parent
f08ceb187f
commit
86f2731da5
@ -1,3 +1,4 @@ |
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ |
||||
export class SealevelInstructionWrapper<Instr> { |
||||
instruction!: number; |
||||
data!: Instr; |
@ -0,0 +1,165 @@ |
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ |
||||
import { PublicKey } from '@solana/web3.js'; |
||||
|
||||
import { Domain } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
SealevelAccountDataWrapper, |
||||
SealevelInstructionWrapper, |
||||
getSealevelAccountDataSchema, |
||||
} from './serialization'; |
||||
|
||||
// TODO move this code to the token package
|
||||
// after we've defined more accurate data schemas for Routers.
|
||||
// Currently the RouterAdapters use this schema as a placeholder
|
||||
|
||||
/** |
||||
* Hyperlane Token Borsh Schema |
||||
*/ |
||||
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs#L25C12-L25C26
|
||||
export class SealevelHyperlaneTokenData { |
||||
/// The bump seed for this PDA.
|
||||
bump!: number; |
||||
/// The address of the mailbox contract.
|
||||
mailbox!: Uint8Array; |
||||
mailbox_pubkey!: PublicKey; |
||||
/// The Mailbox process authority specific to this program as the recipient.
|
||||
mailbox_process_authority!: Uint8Array; |
||||
mailbox_process_authority_pubkey!: PublicKey; |
||||
/// The dispatch authority PDA's bump seed.
|
||||
dispatch_authority_bump!: number; |
||||
/// The decimals of the local token.
|
||||
decimals!: number; |
||||
/// The decimals of the remote token.
|
||||
remote_decimals!: number; |
||||
/// Access control owner.
|
||||
owner?: Uint8Array; |
||||
owner_pub_key?: PublicKey; |
||||
/// The interchain security module.
|
||||
interchain_security_module?: Uint8Array; |
||||
interchain_security_module_pubkey?: PublicKey; |
||||
// The interchain gas paymaster
|
||||
interchain_gas_paymaster?: { |
||||
program_id: Uint8Array; |
||||
type: number; |
||||
account: Uint8Array; |
||||
}; |
||||
interchain_gas_paymaster_pubkey?: PublicKey; |
||||
interchain_gas_paymaster_account_pubkey?: PublicKey; |
||||
// Gas amounts by destination
|
||||
destination_gas?: Map<Domain, bigint>; |
||||
/// Remote routers.
|
||||
remote_routers?: Map<Domain, Uint8Array>; |
||||
remote_router_pubkeys: Map<Domain, PublicKey>; |
||||
constructor(public readonly fields: any) { |
||||
Object.assign(this, fields); |
||||
this.mailbox_pubkey = new PublicKey(this.mailbox); |
||||
this.mailbox_pubkey = new PublicKey(this.mailbox_process_authority); |
||||
this.owner_pub_key = this.owner ? new PublicKey(this.owner) : undefined; |
||||
this.interchain_security_module_pubkey = this.interchain_security_module |
||||
? new PublicKey(this.interchain_security_module) |
||||
: undefined; |
||||
this.interchain_gas_paymaster_pubkey = this.interchain_gas_paymaster |
||||
?.program_id |
||||
? new PublicKey(this.interchain_gas_paymaster.program_id) |
||||
: undefined; |
||||
this.interchain_gas_paymaster_account_pubkey = this.interchain_gas_paymaster |
||||
?.account |
||||
? new PublicKey(this.interchain_gas_paymaster.account) |
||||
: undefined; |
||||
this.remote_router_pubkeys = new Map<number, PublicKey>(); |
||||
if (this.remote_routers) { |
||||
for (const [k, v] of this.remote_routers.entries()) { |
||||
this.remote_router_pubkeys.set(k, new PublicKey(v)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const SealevelHyperlaneTokenDataSchema = new Map<any, any>([ |
||||
[ |
||||
SealevelAccountDataWrapper, |
||||
getSealevelAccountDataSchema(SealevelHyperlaneTokenData), |
||||
], |
||||
[ |
||||
SealevelHyperlaneTokenData, |
||||
{ |
||||
kind: 'struct', |
||||
fields: [ |
||||
['bump', 'u8'], |
||||
['mailbox', [32]], |
||||
['mailbox_process_authority', [32]], |
||||
['dispatch_authority_bump', 'u8'], |
||||
['decimals', 'u8'], |
||||
['remote_decimals', 'u8'], |
||||
['owner', { kind: 'option', type: [32] }], |
||||
['interchain_security_module', { kind: 'option', type: [32] }], |
||||
[ |
||||
'interchain_gas_paymaster', |
||||
{ |
||||
kind: 'option', |
||||
type: { |
||||
kind: 'struct', |
||||
fields: [ |
||||
['program_id', [32]], |
||||
['type', 'u8'], |
||||
['account', [32]], |
||||
], |
||||
}, |
||||
}, |
||||
], |
||||
['destination_gas', { kind: 'map', key: 'u32', value: 'u64' }], |
||||
['remote_routers', { kind: 'map', key: 'u32', value: [32] }], |
||||
], |
||||
}, |
||||
], |
||||
]); |
||||
|
||||
/** |
||||
* Transfer Remote Borsh Schema |
||||
*/ |
||||
|
||||
// Should match Instruction in https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs
|
||||
export enum SealevelHypTokenInstruction { |
||||
Init, |
||||
TransferRemote, |
||||
EnrollRemoteRouter, |
||||
EnrollRemoteRouters, |
||||
SetInterchainSecurityModule, |
||||
TransferOwnership, |
||||
} |
||||
|
||||
export class SealevelTransferRemoteInstruction { |
||||
destination_domain!: number; |
||||
recipient!: Uint8Array; |
||||
recipient_pubkey!: PublicKey; |
||||
amount_or_id!: number; |
||||
constructor(public readonly fields: any) { |
||||
Object.assign(this, fields); |
||||
this.recipient_pubkey = new PublicKey(this.recipient); |
||||
} |
||||
} |
||||
|
||||
export const SealevelTransferRemoteSchema = new Map<any, any>([ |
||||
[ |
||||
SealevelInstructionWrapper, |
||||
{ |
||||
kind: 'struct', |
||||
fields: [ |
||||
['instruction', 'u8'], |
||||
['data', SealevelTransferRemoteInstruction], |
||||
], |
||||
}, |
||||
], |
||||
[ |
||||
SealevelTransferRemoteInstruction, |
||||
{ |
||||
kind: 'struct', |
||||
fields: [ |
||||
['destination_domain', 'u32'], |
||||
['recipient', [32]], |
||||
['amount_or_id', 'u256'], |
||||
], |
||||
}, |
||||
], |
||||
]); |
@ -0,0 +1,40 @@ |
||||
import { Address, Domain } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ERC20Metadata } from '../config'; |
||||
|
||||
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply'>; |
||||
|
||||
export interface TransferParams { |
||||
weiAmountOrId: string | number; |
||||
recipient: Address; |
||||
|
||||
// Solana-specific params
|
||||
// Included here optionally to keep Adapter types simple
|
||||
fromTokenAccount?: Address; |
||||
fromAccountOwner?: Address; |
||||
mailbox?: Address; |
||||
} |
||||
|
||||
export interface TransferRemoteParams extends TransferParams { |
||||
destination: Domain; |
||||
txValue?: string; |
||||
} |
||||
|
||||
export interface ITokenAdapter { |
||||
getBalance(address?: Address): Promise<string>; |
||||
getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata>; |
||||
populateApproveTx(TransferParams: TransferParams): unknown | Promise<unknown>; |
||||
populateTransferTx( |
||||
TransferParams: TransferParams, |
||||
): unknown | Promise<unknown>; |
||||
} |
||||
|
||||
export interface IHypTokenAdapter extends ITokenAdapter { |
||||
getDomains(): Promise<Domain[]>; |
||||
getRouterAddress(domain: Domain): Promise<Buffer>; |
||||
getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>>; |
||||
quoteGasPayment(destination: Domain): Promise<string>; |
||||
populateTransferRemoteTx( |
||||
TransferParams: TransferRemoteParams, |
||||
): unknown | Promise<unknown>; |
||||
} |
@ -0,0 +1,536 @@ |
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
import { |
||||
TOKEN_2022_PROGRAM_ID, |
||||
TOKEN_PROGRAM_ID, |
||||
createTransferInstruction, |
||||
getAssociatedTokenAddressSync, |
||||
} from '@solana/spl-token'; |
||||
import { |
||||
AccountMeta, |
||||
Connection, |
||||
Keypair, |
||||
PublicKey, |
||||
SystemProgram, |
||||
Transaction, |
||||
TransactionInstruction, |
||||
} from '@solana/web3.js'; |
||||
import BigNumber from 'bignumber.js'; |
||||
import { deserializeUnchecked, serialize } from 'borsh'; |
||||
|
||||
import { |
||||
SEALEVEL_SPL_NOOP_ADDRESS, |
||||
SealevelAccountDataWrapper, |
||||
SealevelHypTokenInstruction, |
||||
SealevelHyperlaneTokenData, |
||||
SealevelHyperlaneTokenDataSchema, |
||||
SealevelInstructionWrapper, |
||||
SealevelTransferRemoteInstruction, |
||||
SealevelTransferRemoteSchema, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { |
||||
Address, |
||||
Domain, |
||||
addressToBytes, |
||||
isZeroishAddress, |
||||
} from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
IHypTokenAdapter, |
||||
ITokenAdapter, |
||||
MinimalTokenMetadata, |
||||
TransferParams, |
||||
TransferRemoteParams, |
||||
} from './ITokenAdapter'; |
||||
|
||||
// author @tkporter @jmrossy
|
||||
// Interacts with native currencies
|
||||
export class SealevelNativeTokenAdapter implements ITokenAdapter { |
||||
constructor( |
||||
public readonly connection: Connection, |
||||
public readonly signerAddress?: Address, |
||||
) {} |
||||
|
||||
async getBalance(address?: Address): Promise<string> { |
||||
const pubKey = resolveAddress(address, this.signerAddress); |
||||
const balance = await this.connection.getBalance(pubKey); |
||||
return balance.toString(); |
||||
} |
||||
|
||||
async getMetadata(): Promise<MinimalTokenMetadata> { |
||||
throw new Error('Metadata not available to native tokens'); |
||||
} |
||||
|
||||
populateApproveTx(_params: TransferParams): Transaction { |
||||
throw new Error('Approve not required for native tokens'); |
||||
} |
||||
|
||||
populateTransferTx({ |
||||
weiAmountOrId, |
||||
recipient, |
||||
fromAccountOwner, |
||||
}: TransferParams): Transaction { |
||||
const fromPubkey = resolveAddress(fromAccountOwner, this.signerAddress); |
||||
return new Transaction().add( |
||||
SystemProgram.transfer({ |
||||
fromPubkey, |
||||
toPubkey: new PublicKey(recipient), |
||||
lamports: new BigNumber(weiAmountOrId).toNumber(), |
||||
}), |
||||
); |
||||
} |
||||
} |
||||
|
||||
// Interacts with SPL token programs
|
||||
export class SealevelTokenAdapter implements ITokenAdapter { |
||||
public readonly tokenProgramPubKey: PublicKey; |
||||
|
||||
constructor( |
||||
public readonly connection: Connection, |
||||
public readonly tokenProgramId: Address, |
||||
public readonly isSpl2022: boolean = false, |
||||
public readonly signerAddress?: Address, |
||||
) { |
||||
this.tokenProgramPubKey = new PublicKey(tokenProgramId); |
||||
} |
||||
|
||||
async getBalance(owner: Address): Promise<string> { |
||||
const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner)); |
||||
const response = await this.connection.getTokenAccountBalance(tokenPubKey); |
||||
return response.value.amount; |
||||
} |
||||
|
||||
async getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata> { |
||||
// TODO solana support
|
||||
return { decimals: 9, symbol: 'SPL', name: 'SPL Token' }; |
||||
} |
||||
|
||||
populateApproveTx(_params: TransferParams): Promise<Transaction> { |
||||
throw new Error('Approve not required for sealevel tokens'); |
||||
} |
||||
|
||||
populateTransferTx({ |
||||
weiAmountOrId, |
||||
recipient, |
||||
fromAccountOwner, |
||||
fromTokenAccount, |
||||
}: TransferParams): Transaction { |
||||
if (!fromTokenAccount) throw new Error('No fromTokenAccount provided'); |
||||
const fromWalletPubKey = resolveAddress( |
||||
fromAccountOwner, |
||||
this.signerAddress, |
||||
); |
||||
return new Transaction().add( |
||||
createTransferInstruction( |
||||
new PublicKey(fromTokenAccount), |
||||
new PublicKey(recipient), |
||||
fromWalletPubKey, |
||||
new BigNumber(weiAmountOrId).toNumber(), |
||||
), |
||||
); |
||||
} |
||||
|
||||
getTokenProgramId(): PublicKey { |
||||
return this.isSpl2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID; |
||||
} |
||||
|
||||
deriveAssociatedTokenAccount(owner: PublicKey): PublicKey { |
||||
return getAssociatedTokenAddressSync( |
||||
this.tokenProgramPubKey, |
||||
owner, |
||||
true, |
||||
this.getTokenProgramId(), |
||||
); |
||||
} |
||||
} |
||||
|
||||
export abstract class SealevelHypTokenAdapter |
||||
extends SealevelTokenAdapter |
||||
implements IHypTokenAdapter |
||||
{ |
||||
public readonly warpProgramPubKey: PublicKey; |
||||
|
||||
constructor( |
||||
public readonly connection: Connection, |
||||
public readonly warpRouteProgramId: Address, |
||||
public readonly tokenProgramId: Address, |
||||
public readonly isSpl2022: boolean = false, |
||||
public readonly signerAddress?: Address, |
||||
) { |
||||
// Pass in placeholder address to avoid errors for native token addresses (which as represented here as 0s)
|
||||
const superTokenProgramId = isZeroishAddress(tokenProgramId) |
||||
? SystemProgram.programId.toBase58() |
||||
: tokenProgramId; |
||||
super(connection, superTokenProgramId, isSpl2022, signerAddress); |
||||
this.warpProgramPubKey = new PublicKey(warpRouteProgramId); |
||||
} |
||||
|
||||
async getTokenAccountData(): Promise<SealevelHyperlaneTokenData> { |
||||
const tokenPda = this.deriveHypTokenAccount(); |
||||
const accountInfo = await this.connection.getAccountInfo(tokenPda); |
||||
if (!accountInfo) throw new Error(`No account info found for ${tokenPda}`); |
||||
const wrappedData = deserializeUnchecked( |
||||
SealevelHyperlaneTokenDataSchema, |
||||
SealevelAccountDataWrapper, |
||||
accountInfo.data, |
||||
); |
||||
return wrappedData.data as SealevelHyperlaneTokenData; |
||||
} |
||||
|
||||
override async getMetadata(): Promise<MinimalTokenMetadata> { |
||||
const tokenData = await this.getTokenAccountData(); |
||||
// TODO full token metadata support
|
||||
return { |
||||
decimals: tokenData.decimals, |
||||
symbol: 'HYP', |
||||
name: 'Unknown Hyp Token', |
||||
}; |
||||
} |
||||
|
||||
async getDomains(): Promise<Domain[]> { |
||||
const routers = await this.getAllRouters(); |
||||
return routers.map((router) => router.domain); |
||||
} |
||||
|
||||
async getRouterAddress(domain: Domain): Promise<Buffer> { |
||||
const routers = await this.getAllRouters(); |
||||
const addr = routers.find((router) => router.domain === domain)?.address; |
||||
if (!addr) throw new Error(`No router found for ${domain}`); |
||||
return addr; |
||||
} |
||||
|
||||
async getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>> { |
||||
const tokenData = await this.getTokenAccountData(); |
||||
const domainToPubKey = tokenData.remote_router_pubkeys; |
||||
return Array.from(domainToPubKey.entries()).map(([domain, pubKey]) => ({ |
||||
domain, |
||||
address: pubKey.toBuffer(), |
||||
})); |
||||
} |
||||
|
||||
async quoteGasPayment(destination: Domain): Promise<string> { |
||||
// TODO Solana support
|
||||
return '0'; |
||||
} |
||||
|
||||
async populateTransferRemoteTx({ |
||||
weiAmountOrId, |
||||
destination, |
||||
recipient, |
||||
fromAccountOwner, |
||||
mailbox, |
||||
}: TransferRemoteParams): Promise<Transaction> { |
||||
if (!mailbox) throw new Error('No mailbox provided'); |
||||
const fromWalletPubKey = resolveAddress( |
||||
fromAccountOwner, |
||||
this.signerAddress, |
||||
); |
||||
const randomWallet = Keypair.generate(); |
||||
const mailboxPubKey = new PublicKey(mailbox); |
||||
const keys = this.getTransferInstructionKeyList( |
||||
fromWalletPubKey, |
||||
mailboxPubKey, |
||||
randomWallet.publicKey, |
||||
); |
||||
|
||||
const value = new SealevelInstructionWrapper({ |
||||
instruction: SealevelHypTokenInstruction.TransferRemote, |
||||
data: new SealevelTransferRemoteInstruction({ |
||||
destination_domain: destination, |
||||
recipient: addressToBytes(recipient), |
||||
amount_or_id: new BigNumber(weiAmountOrId).toNumber(), |
||||
}), |
||||
}); |
||||
const serializedData = serialize(SealevelTransferRemoteSchema, value); |
||||
|
||||
const transferRemoteInstruction = new TransactionInstruction({ |
||||
keys, |
||||
programId: this.warpProgramPubKey, |
||||
// Array of 1s is an arbitrary 8 byte "discriminator"
|
||||
// https://github.com/hyperlane-xyz/issues/issues/462#issuecomment-1587859359
|
||||
data: Buffer.concat([ |
||||
Buffer.from([1, 1, 1, 1, 1, 1, 1, 1]), |
||||
Buffer.from(serializedData), |
||||
]), |
||||
}); |
||||
|
||||
const recentBlockhash = ( |
||||
await this.connection.getLatestBlockhash('finalized') |
||||
).blockhash; |
||||
// @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash
|
||||
const tx = new Transaction({ |
||||
feePayer: fromWalletPubKey, |
||||
blockhash: recentBlockhash, |
||||
recentBlockhash, |
||||
}).add(transferRemoteInstruction); |
||||
tx.partialSign(randomWallet); |
||||
return tx; |
||||
} |
||||
|
||||
getTransferInstructionKeyList( |
||||
sender: PublicKey, |
||||
mailbox: PublicKey, |
||||
randomWallet: PublicKey, |
||||
): Array<AccountMeta> { |
||||
return [ |
||||
// 0. [executable] The system program.
|
||||
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, |
||||
// 1. [executable] The spl_noop program.
|
||||
{ |
||||
pubkey: new PublicKey(SEALEVEL_SPL_NOOP_ADDRESS), |
||||
isSigner: false, |
||||
isWritable: false, |
||||
}, |
||||
// 2. [] The token PDA account.
|
||||
{ |
||||
pubkey: this.deriveHypTokenAccount(), |
||||
isSigner: false, |
||||
isWritable: false, |
||||
}, |
||||
// 3. [executable] The mailbox program.
|
||||
{ pubkey: mailbox, isSigner: false, isWritable: false }, |
||||
// 4. [writeable] The mailbox outbox account.
|
||||
{ |
||||
pubkey: this.deriveMailboxOutboxAccount(mailbox), |
||||
isSigner: false, |
||||
isWritable: true, |
||||
}, |
||||
// 5. [] Message dispatch authority.
|
||||
{ |
||||
pubkey: this.deriveMessageDispatchAuthorityAccount(), |
||||
isSigner: false, |
||||
isWritable: false, |
||||
}, |
||||
// 6. [signer] The token sender and mailbox payer.
|
||||
{ pubkey: sender, isSigner: true, isWritable: false }, |
||||
// 7. [signer] Unique message account.
|
||||
{ pubkey: randomWallet, isSigner: true, isWritable: false }, |
||||
// 8. [writeable] Message storage PDA.
|
||||
// prettier-ignore
|
||||
{ pubkey: this.deriveMsgStorageAccount(mailbox, randomWallet), isSigner: false, isWritable: true, }, |
||||
]; |
||||
} |
||||
|
||||
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L19
|
||||
deriveMailboxOutboxAccount(mailbox: PublicKey): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[Buffer.from('hyperlane'), Buffer.from('-'), Buffer.from('outbox')], |
||||
mailbox, |
||||
); |
||||
return pda; |
||||
} |
||||
|
||||
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L57
|
||||
deriveMessageDispatchAuthorityAccount(): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[ |
||||
Buffer.from('hyperlane_dispatcher'), |
||||
Buffer.from('-'), |
||||
Buffer.from('dispatch_authority'), |
||||
], |
||||
this.warpProgramPubKey, |
||||
); |
||||
return pda; |
||||
} |
||||
|
||||
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L33-L37
|
||||
deriveMsgStorageAccount( |
||||
mailbox: PublicKey, |
||||
randomWalletPubKey: PublicKey, |
||||
): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[ |
||||
Buffer.from('hyperlane'), |
||||
Buffer.from('-'), |
||||
Buffer.from('dispatched_message'), |
||||
Buffer.from('-'), |
||||
randomWalletPubKey.toBuffer(), |
||||
], |
||||
mailbox, |
||||
); |
||||
return pda; |
||||
} |
||||
|
||||
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs#LL49C1-L53C30
|
||||
deriveHypTokenAccount(): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[ |
||||
Buffer.from('hyperlane_message_recipient'), |
||||
Buffer.from('-'), |
||||
Buffer.from('handle'), |
||||
Buffer.from('-'), |
||||
Buffer.from('account_metas'), |
||||
], |
||||
this.warpProgramPubKey, |
||||
); |
||||
return pda; |
||||
} |
||||
} |
||||
|
||||
// Interacts with Hyp Native token programs
|
||||
export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { |
||||
public readonly wrappedNative: SealevelNativeTokenAdapter; |
||||
|
||||
constructor( |
||||
public readonly connection: Connection, |
||||
public readonly warpRouteProgramId: Address, |
||||
public readonly tokenProgramId: Address, |
||||
public readonly isSpl2022: boolean = false, |
||||
public readonly signerAddress?: Address, |
||||
) { |
||||
super( |
||||
connection, |
||||
warpRouteProgramId, |
||||
tokenProgramId, |
||||
isSpl2022, |
||||
signerAddress, |
||||
); |
||||
this.wrappedNative = new SealevelNativeTokenAdapter( |
||||
connection, |
||||
signerAddress, |
||||
); |
||||
} |
||||
|
||||
override async getBalance(owner: Address): Promise<string> { |
||||
return this.wrappedNative.getBalance(owner); |
||||
} |
||||
|
||||
override async getMetadata(): Promise<MinimalTokenMetadata> { |
||||
return this.wrappedNative.getMetadata(); |
||||
} |
||||
|
||||
getTransferInstructionKeyList( |
||||
sender: PublicKey, |
||||
mailbox: PublicKey, |
||||
randomWallet: PublicKey, |
||||
): Array<AccountMeta> { |
||||
return [ |
||||
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), |
||||
// 9. [executable] The system program.
|
||||
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, |
||||
// 10. [writeable] The native token collateral PDA account.
|
||||
{ |
||||
pubkey: this.deriveNativeTokenCollateralAccount(), |
||||
isSigner: false, |
||||
isWritable: true, |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-token-native/src/plugin.rs#L26
|
||||
deriveNativeTokenCollateralAccount(): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[ |
||||
Buffer.from('hyperlane_token'), |
||||
Buffer.from('-'), |
||||
Buffer.from('native_collateral'), |
||||
], |
||||
this.warpProgramPubKey, |
||||
); |
||||
return pda; |
||||
} |
||||
} |
||||
|
||||
// Interacts with Hyp Collateral token programs
|
||||
export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter { |
||||
async getBalance(owner: Address): Promise<string> { |
||||
// Special case where the owner is the warp route program ID.
|
||||
// This is because collateral warp routes don't hold escrowed collateral
|
||||
// tokens in their associated token account - instead, they hold them in
|
||||
// the escrow account.
|
||||
if (owner === this.warpRouteProgramId) { |
||||
const collateralAccount = this.deriveEscrowAccount(); |
||||
const response = await this.connection.getTokenAccountBalance( |
||||
collateralAccount, |
||||
); |
||||
return response.value.amount; |
||||
} |
||||
|
||||
return super.getBalance(owner); |
||||
} |
||||
|
||||
override getTransferInstructionKeyList( |
||||
sender: PublicKey, |
||||
mailbox: PublicKey, |
||||
randomWallet: PublicKey, |
||||
): Array<AccountMeta> { |
||||
return [ |
||||
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), |
||||
/// 9. [executable] The SPL token program for the mint.
|
||||
{ pubkey: this.getTokenProgramId(), isSigner: false, isWritable: false }, |
||||
/// 10. [writeable] The mint.
|
||||
{ pubkey: this.tokenProgramPubKey, isSigner: false, isWritable: true }, |
||||
/// 11. [writeable] The token sender's associated token account, from which tokens will be sent.
|
||||
{ |
||||
pubkey: this.deriveAssociatedTokenAccount(sender), |
||||
isSigner: false, |
||||
isWritable: true, |
||||
}, |
||||
/// 12. [writeable] The escrow PDA account.
|
||||
{ pubkey: this.deriveEscrowAccount(), isSigner: false, isWritable: true }, |
||||
]; |
||||
} |
||||
|
||||
deriveEscrowAccount(): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('escrow')], |
||||
this.warpProgramPubKey, |
||||
); |
||||
return pda; |
||||
} |
||||
} |
||||
|
||||
// Interacts with Hyp Synthetic token programs (aka 'HypTokens')
|
||||
export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter { |
||||
override getTransferInstructionKeyList( |
||||
sender: PublicKey, |
||||
mailbox: PublicKey, |
||||
randomWallet: PublicKey, |
||||
): Array<AccountMeta> { |
||||
return [ |
||||
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), |
||||
/// 9. [executable] The spl_token_2022 program.
|
||||
{ pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, |
||||
/// 10. [writeable] The mint / mint authority PDA account.
|
||||
{ |
||||
pubkey: this.deriveMintAuthorityAccount(), |
||||
isSigner: false, |
||||
isWritable: true, |
||||
}, |
||||
/// 11. [writeable] The token sender's associated token account, from which tokens will be burned.
|
||||
{ |
||||
pubkey: this.deriveAssociatedTokenAccount(sender), |
||||
isSigner: false, |
||||
isWritable: true, |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
override async getBalance(owner: Address): Promise<string> { |
||||
const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner)); |
||||
const response = await this.connection.getTokenAccountBalance(tokenPubKey); |
||||
return response.value.amount; |
||||
} |
||||
|
||||
deriveMintAuthorityAccount(): PublicKey { |
||||
const [pda] = PublicKey.findProgramAddressSync( |
||||
[Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('mint')], |
||||
this.warpProgramPubKey, |
||||
); |
||||
return pda; |
||||
} |
||||
|
||||
override deriveAssociatedTokenAccount(owner: PublicKey): PublicKey { |
||||
return getAssociatedTokenAddressSync( |
||||
this.deriveMintAuthorityAccount(), |
||||
new PublicKey(owner), |
||||
true, |
||||
TOKEN_2022_PROGRAM_ID, |
||||
); |
||||
} |
||||
} |
||||
|
||||
function resolveAddress(address1?: Address, address2?: Address): PublicKey { |
||||
if (address1) return new PublicKey(address1); |
||||
else if (address2) return new PublicKey(address2); |
||||
else throw new Error('No address provided'); |
||||
} |
Loading…
Reference in new issue