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
Kunal Arora 1 year ago committed by GitHub
parent f08ceb187f
commit 86f2731da5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      typescript/sdk/src/index.ts
  2. 17
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts
  3. 81
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts
  4. 1
      typescript/sdk/src/sealevel/serialization.ts
  5. 165
      typescript/sdk/src/sealevel/tokenSerialization.ts
  6. 1
      typescript/token/package.json
  7. 40
      typescript/token/src/adapters/ITokenAdapter.ts
  8. 536
      typescript/token/src/adapters/SealevelTokenAdapter.ts
  9. 1
      typescript/token/src/index.ts
  10. 2
      typescript/utils/index.ts
  11. 2
      typescript/utils/src/types.ts
  12. 90
      yarn.lock

@ -237,7 +237,6 @@ export {
export {
SealevelGasRouterAdapter,
SealevelRouterAdapter,
SealevelTokenDataSchema,
} from './router/adapters/SealevelRouterAdapter';
export { IGasRouterAdapter, IRouterAdapter } from './router/adapters/types';
export {
@ -254,6 +253,18 @@ export {
RouterConfig,
proxiedFactories,
} from './router/types';
export {
SealevelAccountDataWrapper,
SealevelInstructionWrapper,
getSealevelAccountDataSchema,
} from './sealevel/serialization';
export {
SealevelHypTokenInstruction,
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
SealevelTransferRemoteInstruction,
SealevelTransferRemoteSchema,
} from './sealevel/tokenSerialization';
export {
createRouterConfigMap,
deployTestIgpsAndGetRouterConfig,
@ -268,9 +279,4 @@ export {
export { MultiGeneric } from './utils/MultiGeneric';
export { filterByChains } from './utils/filter';
export { multisigIsmVerificationCost } from './utils/ism';
export {
SealevelAccountDataWrapper,
SealevelInstructionWrapper,
getSealevelAccountDataSchema,
} from './utils/sealevel';
export { chainMetadataToWagmiChain, wagmiChainMetadata } from './utils/wagmi';

@ -1,30 +1,29 @@
import { deserializeUnchecked } from 'borsh';
import { expect } from 'chai';
import { SealevelAccountDataWrapper } from '../../utils/sealevel';
import { SealevelAccountDataWrapper } from '../../sealevel/serialization';
import {
SealevelTokenData,
SealevelTokenDataSchema,
} from './SealevelRouterAdapter';
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
} from '../../sealevel/tokenSerialization';
// Copied from the warp token router program on Solana devnet
const RAW_ACCOUNT_INFO =
'01fe3a280e8466d26bc4e1a5d3d17e73f7b307c082156dd0ffbf8c5f9ae75506d6f1e9fd9f0b53dbdafc27f42dc88acef25b6cea358c214ced8144165b842148eddbfe0606014d8d3746cf8844ccba8c2f0662b0f1be404229c75c88c93985bd07881eea18f5000200000069a80000000000000000000000000000a97f4eacbc363f82d25a540440afc6f78920299b742d627aa7b75e9d68d3a8f9a84720e039cbb1f9ef1c515f2b3aa99619ecb6f07792bde406ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9e92839550965ffd4d64acaaf46d45df7318e5b4f57c90c487d60625d829b837b36b9e506fe298843d50359492a8b4ce739c8fa85da27ac0bb53a1ad5fa66fcc9fffd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
'01ff3a280e8466d26bc4e1a5d3d17e73f7b307c082156dd0ffbf8c5f9ae75506d6f14aed87b9d3a2bb5effdbdcd1af363555ff8b6c1311a93c495e6bc722284d2574fb0612012cbc3cc37a2d2e8aaa301fac7e032fbe5d3140f8a12d7445e7fc69f80f60105800000200000061000000a009010000000000c2570100e0ab000000000000020000006100000000000000000000000000000031b5234a896fbc4b3e2f7237592d054716762131c257010000000000000000000000000034a9af13c5555bad0783c220911b9ef59cfdbcef06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9e92839550965ffd4d64acaaf46d45df7318e5b4f57c90c487d60625d829b837b256d8b6f7c1f678a52ef123ddc35c248fcc1e1895e5b8c6d5e6dd381f8090a48fffe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
const OWNER_PUB_KEY = '6DjHX6Ezjpq3zZMZ8KsqyoFYo1zPSDoiZmLLkxD4xKXS';
const OWNER_PUB_KEY = '41dRB2nrYY8Ymjctq4HNa3uF7gRG829pswAjbDtsj6vK';
describe('SealevelRouterAdapter', () => {
describe('account info', () => {
it('correctly deserializes router account info', () => {
const rawData = Buffer.from(RAW_ACCOUNT_INFO, 'hex');
const wrappedData = deserializeUnchecked(
SealevelTokenDataSchema,
SealevelHyperlaneTokenDataSchema,
SealevelAccountDataWrapper,
rawData,
);
expect(wrappedData.initialized).to.eql(1);
const data = wrappedData.data as SealevelTokenData;
const data = wrappedData.data as SealevelHyperlaneTokenData;
expect(data.decimals).to.eql(6);
expect(data.owner_pub_key?.toBase58()).to.eql(OWNER_PUB_KEY);
expect(data.remote_router_pubkeys.size).to.eql(2);

@ -5,79 +5,16 @@ import { deserializeUnchecked } from 'borsh';
import { Address, Domain } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import { ChainName } from '../../types';
import { SealevelAccountDataWrapper } from '../../sealevel/serialization';
import {
SealevelAccountDataWrapper,
getSealevelAccountDataSchema,
} from '../../utils/sealevel';
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
} from '../../sealevel/tokenSerialization';
import { ChainName } from '../../types';
import { RouterAddress } from '../types';
import { IGasRouterAdapter, IRouterAdapter } from './types';
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs
export class SealevelTokenData {
/// 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;
/// 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.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));
}
}
}
}
// Hyperlane Token Borsh Schema
export const SealevelTokenDataSchema = new Map<any, any>([
[SealevelAccountDataWrapper, getSealevelAccountDataSchema(SealevelTokenData)],
[
SealevelTokenData,
{
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] }],
['remote_routers', { kind: 'map', key: 'u32', value: [32] }],
],
},
],
]);
export class SealevelRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
>
@ -128,7 +65,9 @@ export class SealevelRouterAdapter<
// TODO this incorrectly assumes all sealevel routers will have the TokenRouter's data schema
// This will need to change when other types of routers are supported
async getRouterAccountInfo(chain: ChainName): Promise<SealevelTokenData> {
async getRouterAccountInfo(
chain: ChainName,
): Promise<SealevelHyperlaneTokenData> {
const address = this.multiProvider.getChainMetadata(chain).router;
const connection = this.multiProvider.getSolanaWeb3Provider(chain);
@ -139,11 +78,11 @@ export class SealevelRouterAdapter<
`No account info found for ${msgRecipientPda.toBase58()}}`,
);
const accountData = deserializeUnchecked(
SealevelTokenDataSchema,
SealevelHyperlaneTokenDataSchema,
SealevelAccountDataWrapper,
accountInfo.data,
);
return accountData.data as SealevelTokenData;
return accountData.data as SealevelHyperlaneTokenData;
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs

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

@ -7,6 +7,7 @@
"@hyperlane-xyz/sdk": "1.4.2",
"@hyperlane-xyz/utils": "1.4.2",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"@solana/spl-token": "^0.3.8",
"ethers": "^5.7.2"
},
"devDependencies": {

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

@ -1,3 +1,4 @@
export { SealevelHypCollateralAdapter } from './adapters/SealevelTokenAdapter';
export { HypERC20App, HypERC721App } from './app';
export {
CollateralConfig,

@ -96,6 +96,8 @@ export {
Address,
AddressBytes32,
CallData,
ChainCaip19Id,
ChainCaip2Id,
Checkpoint,
Domain,
HexString,

@ -17,6 +17,8 @@ export const ProtocolSmallestUnit = {
export type Domain = number;
export type Address = string;
export type AddressBytes32 = string;
export type ChainCaip2Id = `${string}:${string}`; // e.g. ethereum:1 or solana:mainnet-beta
export type ChainCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f
export type HexString = string;
// copied from node_modules/@ethersproject/bytes/src.ts/index.ts

@ -2636,6 +2636,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.22.6":
version: 7.22.10
resolution: "@babel/runtime@npm:7.22.10"
dependencies:
regenerator-runtime: ^0.14.0
checksum: 524d41517e68953dbc73a4f3616b8475e5813f64e28ba89ff5fca2c044d535c2ea1a3f310df1e5bb06162e1f0b401b5c4af73fe6e2519ca2450d9d8c44cf268d
languageName: node
linkType: hard
"@babel/template@npm:^7.12.13, @babel/template@npm:^7.16.7":
version: 7.16.7
resolution: "@babel/template@npm:7.16.7"
@ -3991,6 +4000,7 @@ __metadata:
"@nomiclabs/hardhat-ethers": ^2.2.1
"@nomiclabs/hardhat-waffle": ^2.0.3
"@openzeppelin/contracts-upgradeable": ^4.8.0
"@solana/spl-token": ^0.3.8
"@trivago/prettier-plugin-sort-imports": ^3.2.0
"@typechain/ethers-v5": 10.0.0
"@typechain/hardhat": ^6.0.0
@ -4327,7 +4337,7 @@ __metadata:
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.1, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:~1.3.0":
"@noble/hashes@npm:1.3.1, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0":
version: 1.3.1
resolution: "@noble/hashes@npm:1.3.1"
checksum: 7fdefc0f7a0c1ec27acc6ff88841793e3f93ec4ce6b8a6a12bfc0dd70ae6b7c4c82fe305fdfeda1735d5ad4a9eebe761e6693b3d355689c559e91242f4bc95b1
@ -5167,6 +5177,18 @@ __metadata:
languageName: node
linkType: hard
"@solana/buffer-layout-utils@npm:^0.2.0":
version: 0.2.0
resolution: "@solana/buffer-layout-utils@npm:0.2.0"
dependencies:
"@solana/buffer-layout": ^4.0.0
"@solana/web3.js": ^1.32.0
bigint-buffer: ^1.1.5
bignumber.js: ^9.0.1
checksum: 9284242245b18b49577195ba7548263850be865a4a2d183944fa01bb76382039db589aab8473698e9bb734b515ada9b4d70db0a72e341c5d567c59b83d6d0840
languageName: node
linkType: hard
"@solana/buffer-layout@npm:^4.0.0":
version: 4.0.1
resolution: "@solana/buffer-layout@npm:4.0.1"
@ -5176,6 +5198,42 @@ __metadata:
languageName: node
linkType: hard
"@solana/spl-token@npm:^0.3.8":
version: 0.3.8
resolution: "@solana/spl-token@npm:0.3.8"
dependencies:
"@solana/buffer-layout": ^4.0.0
"@solana/buffer-layout-utils": ^0.2.0
buffer: ^6.0.3
peerDependencies:
"@solana/web3.js": ^1.47.4
checksum: 01f4f87112b0ad277701a3bcb8e03069b69449b92724b17959107686731082bfd3475b5f105e1e8f04badd2e810a43d5ef811744ced5178eea1232de8fd75147
languageName: node
linkType: hard
"@solana/web3.js@npm:^1.32.0":
version: 1.78.4
resolution: "@solana/web3.js@npm:1.78.4"
dependencies:
"@babel/runtime": ^7.22.6
"@noble/curves": ^1.0.0
"@noble/hashes": ^1.3.1
"@solana/buffer-layout": ^4.0.0
agentkeepalive: ^4.3.0
bigint-buffer: ^1.1.5
bn.js: ^5.2.1
borsh: ^0.7.0
bs58: ^4.0.1
buffer: 6.0.3
fast-stable-stringify: ^1.0.0
jayson: ^4.1.0
node-fetch: ^2.6.12
rpc-websockets: ^7.5.1
superstruct: ^0.14.2
checksum: e1c44c6cbec87cdfd4d6d23b4241b746e14ed3a9ca73d596693758d91ac825cecf579345da3b0b7bb5e54b6794791bc0eac02cadf11f1ec79e859b6536f26f11
languageName: node
linkType: hard
"@solana/web3.js@npm:^1.78.0":
version: 1.78.0
resolution: "@solana/web3.js@npm:1.78.0"
@ -6054,6 +6112,15 @@ __metadata:
languageName: node
linkType: hard
"agentkeepalive@npm:^4.3.0":
version: 4.5.0
resolution: "agentkeepalive@npm:4.5.0"
dependencies:
humanize-ms: ^1.2.1
checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481
languageName: node
linkType: hard
"aggregate-error@npm:^3.0.0":
version: 3.1.0
resolution: "aggregate-error@npm:3.1.0"
@ -14737,6 +14804,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.12":
version: 2.6.13
resolution: "node-fetch@npm:2.6.13"
dependencies:
whatwg-url: ^5.0.0
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 055845ae5b4796c78c7053564745345025cf959563b3568b43c385f67d311779e6b00e5fef6ed1b79f86ba4048e4b4b722e1aa948305521b9353eb7e788912c9
languageName: node
linkType: hard
"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0":
version: 4.4.0
resolution: "node-gyp-build@npm:4.4.0"
@ -16183,6 +16264,13 @@ __metadata:
languageName: node
linkType: hard
"regenerator-runtime@npm:^0.14.0":
version: 0.14.0
resolution: "regenerator-runtime@npm:0.14.0"
checksum: 1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3
languageName: node
linkType: hard
"regenerator-transform@npm:^0.10.0":
version: 0.10.1
resolution: "regenerator-transform@npm:0.10.1"

Loading…
Cancel
Save