Finish migrating warp UI utils + adapters to SDK (#2711)

### Description

- Build on #2684, migrates the EvmAdapter and missing utils from the warp UI
- Remove the token App as it's unused and redundant with adapters
- Add IGP serialization code for #2546 
- Simplify multi-protocol adapters in SDK
- Make token adapters extend base adapters
- Move token serialization code to token package

### Drive-by changes

- Update Sepolia RPC which was causing errors

### Related issues

#2652 

### Backward compatibility

Yes

### Testing

Tested these in warp UI

---------

Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
pull/2732/head v1.5.1
J M Rossy 1 year ago committed by GitHub
parent 35fdc74f7c
commit 7a5dce222d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      typescript/helloworld/src/multiProtocolApp/evmAdapter.ts
  2. 10
      typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts
  3. 51
      typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts
  4. 9
      typescript/helloworld/src/multiProtocolApp/types.ts
  5. 18
      typescript/infra/scripts/helloworld/utils.ts
  6. 21
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  7. 9
      typescript/sdk/src/app/MultiProtocolApp.test.ts
  8. 86
      typescript/sdk/src/app/MultiProtocolApp.ts
  9. 36
      typescript/sdk/src/consts/chainMetadata.ts
  10. 8
      typescript/sdk/src/core/MultiProtocolCore.test.ts
  11. 19
      typescript/sdk/src/core/MultiProtocolCore.ts
  12. 54
      typescript/sdk/src/core/adapters/EvmCoreAdapter.ts
  13. 18
      typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts
  14. 3
      typescript/sdk/src/core/adapters/types.ts
  15. 4
      typescript/sdk/src/core/contracts.ts
  16. 58
      typescript/sdk/src/gas/adapters/SealevelIgpAdapter.ts
  17. 92
      typescript/sdk/src/gas/adapters/serialization.ts
  18. 26
      typescript/sdk/src/index.ts
  19. 67
      typescript/sdk/src/metadata/ChainMetadataManager.ts
  20. 17
      typescript/sdk/src/providers/MultiProtocolProvider.ts
  21. 27
      typescript/sdk/src/providers/MultiProvider.ts
  22. 5
      typescript/sdk/src/router/MultiProtocolRouterApps.test.ts
  23. 24
      typescript/sdk/src/router/MultiProtocolRouterApps.ts
  24. 78
      typescript/sdk/src/router/adapters/EvmRouterAdapter.ts
  25. 32
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts
  26. 107
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts
  27. 29
      typescript/sdk/src/router/adapters/types.ts
  28. 8
      typescript/sdk/src/utils/sealevelSerialization.ts
  29. 212
      typescript/token/src/adapters/EvmTokenAdapter.ts
  30. 7
      typescript/token/src/adapters/ITokenAdapter.ts
  31. 370
      typescript/token/src/adapters/SealevelTokenAdapter.ts
  32. 35
      typescript/token/src/adapters/serialization.ts
  33. 80
      typescript/token/src/app.ts
  34. 1
      typescript/token/src/config.ts
  35. 30
      typescript/token/src/index.ts
  36. 5
      typescript/utils/index.ts
  37. 13
      typescript/utils/src/addresses.ts
  38. 42
      typescript/utils/src/amount.ts
  39. 23
      typescript/utils/src/big-numbers.ts
  40. 2
      typescript/utils/src/types.ts

@ -2,8 +2,8 @@ import {
ChainName,
EthersV5Transaction,
EvmRouterAdapter,
MultiProtocolProvider,
ProviderType,
RouterAddress,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
@ -13,19 +13,27 @@ import { HelloWorld, HelloWorld__factory } from '../types';
import { IHelloWorldAdapter } from './types';
export class EvmHelloWorldAdapter
extends EvmRouterAdapter<RouterAddress & { mailbox: Address }>
extends EvmRouterAdapter
implements IHelloWorldAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { router: Address; mailbox: Address },
) {
super(chainName, multiProvider, addresses);
}
async populateSendHelloTx(
origin: ChainName,
destination: ChainName,
message: string,
value: string,
): Promise<EthersV5Transaction> {
const contract = this.getConnectedContract(origin);
const contract = this.getConnectedContract();
const toDomain = this.multiProvider.getDomainId(destination);
const { transactionOverrides } =
this.multiProvider.getChainMetadata(origin);
const { transactionOverrides } = this.multiProvider.getChainMetadata(
this.chainName,
);
// apply gas buffer due to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/634
const estimated = await contract.estimateGas.sendHelloWorld(
@ -48,23 +56,27 @@ export class EvmHelloWorldAdapter
}
async channelStats(
origin: ChainName,
destination: ChainName,
destinationMailbox: Address,
): Promise<StatCounts> {
const originDomain = this.multiProvider.getDomainId(origin);
const originDomain = this.multiProvider.getDomainId(this.chainName);
const destinationDomain = this.multiProvider.getDomainId(destination);
const sent = await this.getConnectedContract(origin).sentTo(
destinationDomain,
);
const received = await this.getConnectedContract(destination).sentTo(
originDomain,
const originContract = this.getConnectedContract();
const sent = await originContract.sentTo(destinationDomain);
const destinationProvider =
this.multiProvider.getEthersV5Provider(destination);
const destinationContract = HelloWorld__factory.connect(
destinationMailbox,
destinationProvider,
);
const received = await destinationContract.sentTo(originDomain);
return { sent: sent.toNumber(), received: received.toNumber() };
}
override getConnectedContract(chain: ChainName): HelloWorld {
const address = this.multiProvider.getChainMetadata(chain).router;
const provider = this.multiProvider.getEthersV5Provider(chain);
return HelloWorld__factory.connect(address, provider);
override getConnectedContract(): HelloWorld {
return HelloWorld__factory.connect(
this.addresses.router,
this.getProvider(),
);
}
}

@ -14,8 +14,8 @@ import { SealevelHelloWorldAdapter } from './sealevelAdapter';
import { IHelloWorldAdapter } from './types';
export class HelloMultiProtocolApp extends MultiProtocolRouterApp<
RouterAddress & { mailbox: Address },
IHelloWorldAdapter
IHelloWorldAdapter,
RouterAddress & { mailbox: Address }
> {
override protocolToAdapter(protocol: ProtocolType) {
if (protocol === ProtocolType.Ethereum) return EvmHelloWorldAdapter;
@ -31,7 +31,6 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp<
sender: Address,
): Promise<TypedTransaction> {
return this.adapter(origin).populateSendHelloTx(
origin,
destination,
message,
value,
@ -40,7 +39,10 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp<
}
channelStats(origin: ChainName, destination: ChainName): Promise<StatCounts> {
return this.adapter(origin).channelStats(origin, destination);
return this.adapter(origin).channelStats(
destination,
this.addresses[destination].mailbox,
);
}
async stats(): Promise<ChainMap<ChainMap<StatCounts>>> {

@ -9,14 +9,15 @@ import {
import { deserializeUnchecked, serialize } from 'borsh';
import {
BaseSealevelAdapter,
ChainName,
MultiProtocolProvider,
ProviderType,
RouterAddress,
SEALEVEL_SPL_NOOP_ADDRESS,
SealevelAccountDataWrapper,
SealevelCoreAdapter,
SealevelInstructionWrapper,
SealevelInterchainGasPaymasterConfig,
SealevelInterchainGasPaymasterConfigSchema,
SealevelRouterAdapter,
SolanaWeb3Transaction,
getSealevelAccountDataSchema,
@ -28,11 +29,18 @@ import { StatCounts } from '../app/types';
import { IHelloWorldAdapter } from './types';
export class SealevelHelloWorldAdapter
extends SealevelRouterAdapter<RouterAddress & { mailbox: Address }>
extends SealevelRouterAdapter
implements IHelloWorldAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { router: Address; mailbox: Address },
) {
super(chainName, multiProvider, addresses);
}
async populateSendHelloTx(
origin: ChainName,
destination: ChainName,
message: string,
value: string,
@ -40,14 +48,13 @@ export class SealevelHelloWorldAdapter
): Promise<SolanaWeb3Transaction> {
this.logger(
'Creating sendHelloWorld tx for sealevel',
origin,
this.chainName,
destination,
message,
value,
);
const { mailbox, router: programId } =
this.multiProvider.getChainMetadata(origin);
const { mailbox, router: programId } = this.addresses;
const mailboxPubKey = new PublicKey(mailbox);
const senderPubKey = new PublicKey(sender);
const programPubKey = new PublicKey(programId);
@ -75,7 +82,7 @@ export class SealevelHelloWorldAdapter
data: Buffer.from(serializedData),
});
const connection = this.multiProvider.getSolanaWeb3Provider(origin);
const connection = this.getProvider();
const recentBlockhash = (await connection.getLatestBlockhash('finalized'))
.blockhash;
// @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash
@ -151,23 +158,20 @@ export class SealevelHelloWorldAdapter
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/dd7ff727b0d3d393a159afa5f0a364775bde3a58/rust/sealevel/programs/helloworld/src/processor.rs#L44
deriveProgramStoragePDA(programId: string | PublicKey): PublicKey {
return BaseSealevelAdapter.derivePda(
return this.derivePda(
['hello_world', '-', 'handle', '-', 'storage'],
programId,
);
}
async channelStats(
origin: ChainName,
_destination: ChainName,
): Promise<StatCounts> {
const data = await this.getAccountInfo(origin);
async channelStats(_destination: ChainName): Promise<StatCounts> {
const data = await this.getAccountInfo();
return { sent: data.sent, received: data.received };
}
async getAccountInfo(chain: ChainName): Promise<HelloWorldData> {
const address = this.multiProvider.getChainMetadata(chain).router;
const connection = this.multiProvider.getSolanaWeb3Provider(chain);
async getAccountInfo(): Promise<HelloWorldData> {
const address = this.addresses.router;
const connection = this.getProvider();
const msgRecipientPda = this.deriveMessageRecipientPda(address);
const accountInfo = await connection.getAccountInfo(msgRecipientPda);
@ -290,14 +294,7 @@ export const HelloWorldDataSchema = new Map<any, any>([
'igp',
{
kind: 'option',
type: {
kind: 'struct',
fields: [
['program_id', [32]],
['type', 'u8'],
['igp_account', [32]],
],
},
type: SealevelInterchainGasPaymasterConfig,
},
],
['owner', { kind: 'option', type: [32] }],
@ -309,4 +306,8 @@ export const HelloWorldDataSchema = new Map<any, any>([
],
},
],
[
SealevelInterchainGasPaymasterConfig,
SealevelInterchainGasPaymasterConfigSchema,
],
]);

@ -1,25 +1,24 @@
import {
ChainName,
IRouterAdapter,
RouterAddress,
TypedTransaction,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { StatCounts } from '../app/types';
export interface IHelloWorldAdapter
extends IRouterAdapter<RouterAddress & { mailbox: Address }> {
export interface IHelloWorldAdapter extends IRouterAdapter {
populateSendHelloTx: (
origin: ChainName,
destination: ChainName,
message: string,
value: string,
sender: Address,
) => Promise<TypedTransaction>;
// TODO break apart into separate origin + destination methods to
// handle case where origin/dest protocols differ
channelStats: (
origin: ChainName,
destination: ChainName,
destinationMailbox: Address,
) => Promise<StatCounts>;
}

@ -17,7 +17,7 @@ import {
igpFactories,
} from '@hyperlane-xyz/sdk';
import { hyperlaneEnvironmentsWithSealevel } from '@hyperlane-xyz/sdk/src';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { ProtocolType, objMerge } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts';
import { EnvironmentConfig } from '../../src/config';
@ -74,20 +74,20 @@ export async function getHelloWorldMultiProtocolApp(
if (!multiProtocolProvider.getKnownChainNames().includes('solanadevnet')) {
multiProtocolProvider.addChain(chainMetadata.solanadevnet);
}
// Add the helloWorld contract addresses to the metadata
const mpWithHelloWorld = multiProtocolProvider.extendChainMetadata(
helloworldConfig.addresses,
);
const core = MultiProtocolCore.fromAddressesMap(
hyperlaneEnvironmentsWithSealevel[sdkEnvName],
multiProtocolProvider,
);
// Extend the MP with mailbox addresses because the sealevel
// adapter needs that to function
const mpWithMailbox = mpWithHelloWorld.extendChainMetadata(core.chainMap);
const app = new HelloMultiProtocolApp(mpWithMailbox);
const routersAndMailboxes = objMerge(
core.chainMap,
helloworldConfig.addresses,
);
const app = new HelloMultiProtocolApp(
multiProtocolProvider,
routersAndMailboxes,
);
// TODO we need a MultiProtocolIgp
// Using an standard IGP for just evm chains for now

@ -1,4 +1,4 @@
import { Connection } from '@solana/web3.js';
import { SystemProgram } from '@solana/web3.js';
import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import yargs from 'yargs';
@ -8,7 +8,12 @@ import {
SealevelHypCollateralAdapter,
TokenType,
} from '@hyperlane-xyz/hyperlane-token';
import { ChainMap, ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import {
ChainMap,
ChainName,
MultiProtocolProvider,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import {
ProtocolType,
debug,
@ -92,11 +97,15 @@ async function checkBalance(
ethers.utils.formatUnits(collateralBalance, token.decimals),
);
} else {
const connection = new Connection(multiprovider.getRpcUrl(chain));
const adapter = new SealevelHypCollateralAdapter(
connection,
token.hypCollateralAddress,
token.address,
chain,
MultiProtocolProvider.fromMultiProvider(multiprovider),
{
token: token.address,
warpRouter: token.hypCollateralAddress,
// Mailbox only required for transfers, using system as placeholder
mailbox: SystemProgram.programId.toBase58(),
},
token.isSpl2022,
);
const collateralBalance = ethers.BigNumber.from(

@ -6,12 +6,13 @@ import { Chains } from '../consts/chains';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import {
BaseAppAdapter,
BaseEvmAdapter,
BaseSealevelAdapter,
MultiProtocolApp,
} from './MultiProtocolApp';
class TestMultiProtocolApp extends MultiProtocolApp {
class TestMultiProtocolApp extends MultiProtocolApp<BaseAppAdapter> {
override protocolToAdapter(protocol: ProtocolType) {
if (protocol === ProtocolType.Ethereum) return BaseEvmAdapter;
if (protocol === ProtocolType.Sealevel) return BaseSealevelAdapter;
@ -23,9 +24,11 @@ describe('MultiProtocolApp', () => {
describe('constructs', () => {
const multiProvider = new MultiProtocolProvider();
it('creates an app class and gleans types from generic', async () => {
const app = new TestMultiProtocolApp(multiProvider);
const app = new TestMultiProtocolApp(multiProvider, {});
expect(app).to.be.instanceOf(MultiProtocolApp);
expect(app.adapter(Chains.ethereum).protocol).to.eql(Chains.ethereum);
expect(app.adapter(Chains.ethereum).protocol).to.eql(
ProtocolType.Ethereum,
);
});
});
});

@ -1,10 +1,20 @@
import { PublicKey } from '@solana/web3.js';
import debug from 'debug';
import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import {
Address,
ProtocolType,
objMap,
promiseObjAll,
} from '@hyperlane-xyz/utils';
import { ChainMetadata } from '../metadata/chainMetadataTypes';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import {
EthersV5Provider,
SolanaWeb3Provider,
TypedProvider,
} from '../providers/ProviderType';
import { ChainMap, ChainName } from '../types';
import { MultiGeneric } from '../utils/MultiGeneric';
@ -14,29 +24,38 @@ import { MultiGeneric } from '../utils/MultiGeneric';
* E.g. EvmRouterAdapter implements EVM-specific router functionality
* whereas SealevelRouterAdapter implements the same logic for Solana
*/
export abstract class BaseAppAdapter<ContractAddrs = {}> {
export abstract class BaseAppAdapter {
public abstract readonly protocol: ProtocolType;
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider<any>,
public readonly addresses: Record<string, Address>,
public readonly logger = debug(`hyperlane:AppAdapter`),
) {}
}
export type AdapterClassType<ContractAddrs = {}, API = {}> = new (
multiProvider: MultiProtocolProvider<ContractAddrs>,
export type AdapterClassType<API> = new (
chainName: ChainName,
multiProvider: MultiProtocolProvider<any>,
addresses: any,
...args: any
) => API;
export class BaseEvmAdapter<
ContractAddrs = {},
> extends BaseAppAdapter<ContractAddrs> {
export class BaseEvmAdapter extends BaseAppAdapter {
public readonly protocol: ProtocolType = ProtocolType.Ethereum;
public getProvider(): EthersV5Provider['provider'] {
return this.multiProvider.getEthersV5Provider(this.chainName);
}
}
export class BaseSealevelAdapter<
ContractAddrs = {},
> extends BaseAppAdapter<ContractAddrs> {
export class BaseSealevelAdapter extends BaseAppAdapter {
public readonly protocol: ProtocolType = ProtocolType.Sealevel;
public getProvider(): SolanaWeb3Provider['provider'] {
return this.multiProvider.getSolanaWeb3Provider(this.chainName);
}
static derivePda(
seeds: Array<string | Buffer>,
programId: string | PublicKey,
@ -47,6 +66,14 @@ export class BaseSealevelAdapter<
);
return pda;
}
// An dynamic alias for static method above for convenience
derivePda(
seeds: Array<string | Buffer>,
programId: string | PublicKey,
): PublicKey {
return BaseSealevelAdapter.derivePda(seeds, programId);
}
}
/**
@ -67,31 +94,26 @@ export class BaseSealevelAdapter<
* @override protocolToAdapter - This should return an Adapter class for a given protocol type
*/
export abstract class MultiProtocolApp<
ContractAddrs = {},
IAdapterApi extends BaseAppAdapter = BaseAppAdapter,
> extends MultiGeneric<ChainMetadata<ContractAddrs>> {
IAdapterApi extends BaseAppAdapter,
ContractAddrs extends Record<string, Address> = {},
> extends MultiGeneric<ChainMetadata> {
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: ChainMap<ContractAddrs>,
public readonly logger = debug('hyperlane:MultiProtocolApp'),
) {
super(multiProvider.metadata);
}
// Subclasses should override this with more specific adapters
// Subclasses must implement this with their specific adapters
abstract protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi>;
metadata(chain: ChainName): ChainMetadata<ContractAddrs> {
return this.get(chain);
}
): AdapterClassType<IAdapterApi>;
// Subclasses may want to override this to provide adapters more arguments
adapter(chain: ChainName): IAdapterApi {
const metadata = this.metadata(chain);
const Adapter = this.protocolToAdapter(metadata.protocol);
if (!Adapter)
throw new Error(`No adapter for protocol ${metadata.protocol}`);
return new Adapter(this.multiProvider);
const Adapter = this.protocolToAdapter(this.protocol(chain));
return new Adapter(chain, this.multiProvider, this.addresses[chain]);
}
adapters(): ChainMap<IAdapterApi> {
@ -103,4 +125,16 @@ export abstract class MultiProtocolApp<
): Promise<ChainMap<Output>> {
return promiseObjAll(objMap(this.adapters(), fn));
}
metadata(chain: ChainName): ChainMetadata {
return this.get(chain);
}
protocol(chain: ChainName): ProtocolType {
return this.metadata(chain).protocol;
}
provider(chain: ChainName): TypedProvider {
return this.multiProvider.getProvider(chain);
}
}

@ -316,10 +316,7 @@ export const sepolia: ChainMetadata = {
protocol: ProtocolType.Ethereum,
displayName: 'Sepolia',
nativeToken: etherToken,
rpcUrls: [
{ http: 'https://endpoints.omniatech.io/v1/eth/sepolia/public' },
{ http: 'https://rpc.sepolia.org' },
],
rpcUrls: [{ http: 'https://endpoints.omniatech.io/v1/eth/sepolia/public' }],
blockExplorers: [
{
name: 'Etherscan',
@ -668,9 +665,9 @@ export const solana: ChainMetadata = {
rpcUrls: [{ http: 'https://api.mainnet-beta.solana.com' }],
blockExplorers: [
{
name: 'SolScan',
url: 'https://solscan.io',
apiUrl: 'https://public-api.solscan.io',
name: 'Solana Explorer',
url: 'https://explorer.solana.com',
apiUrl: 'https://explorer.solana.com',
family: ExplorerFamily.Other,
},
],
@ -690,6 +687,14 @@ export const solanatestnet: ChainMetadata = {
displayNameShort: 'Sol Testnet',
nativeToken: solToken,
rpcUrls: [{ http: 'https://api.testnet.solana.com' }],
blockExplorers: [
{
name: 'Solana Explorer',
url: 'https://explorer.solana.com',
apiUrl: 'https://explorer.solana.com',
family: ExplorerFamily.Other,
},
],
blocks: {
confirmations: 1,
reorgPeriod: 0,
@ -707,6 +712,14 @@ export const solanadevnet: ChainMetadata = {
displayNameShort: 'Sol Devnet',
nativeToken: solToken,
rpcUrls: [{ http: 'https://api.devnet.solana.com' }],
blockExplorers: [
{
name: 'Solana Explorer',
url: 'https://explorer.solana.com',
apiUrl: 'https://explorer.solana.com',
family: ExplorerFamily.Other,
},
],
blocks: {
confirmations: 1,
reorgPeriod: 0,
@ -744,8 +757,9 @@ export const chainMetadata: ChainMap<ChainMetadata> = {
test1,
test2,
test3,
solanadevnet,
solana,
solanatestnet,
solanadevnet,
nautilus,
};
@ -762,3 +776,9 @@ export const mainnetChainsMetadata: Array<ChainMetadata> = Mainnets.map(
export const testnetChainsMetadata: Array<ChainMetadata> = Testnets.map(
(chainName) => chainMetadata[chainName],
);
export const solanaChainToClusterName: ChainMap<string> = {
solana: 'mainnet-beta',
solanatestnet: 'testnet',
solanadevnet: 'devnet',
};

@ -7,20 +7,22 @@ import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import { MultiProtocolCore } from './MultiProtocolCore';
import { EvmCoreAdapter } from './adapters/EvmCoreAdapter';
import { CoreAddresses } from './contracts';
describe('MultiProtocolCore', () => {
describe('constructs', () => {
it('with constructor', async () => {
const multiProvider = new MultiProtocolProvider<CoreAddresses>({
const multiProvider = new MultiProtocolProvider({
ethereum: {
...ethereum,
},
});
const core = new MultiProtocolCore(multiProvider, {
ethereum: {
validatorAnnounce: ethers.constants.AddressZero,
proxyAdmin: ethers.constants.AddressZero,
mailbox: ethers.constants.AddressZero,
},
});
const core = new MultiProtocolCore(multiProvider);
expect(core).to.be.instanceOf(MultiProtocolCore);
const ethAdapter = core.adapter(Chains.ethereum);
expect(ethAdapter).to.be.instanceOf(EvmCoreAdapter);

@ -17,14 +17,15 @@ import { ICoreAdapter } from './adapters/types';
import { CoreAddresses } from './contracts';
export class MultiProtocolCore extends MultiProtocolApp<
CoreAddresses,
ICoreAdapter
ICoreAdapter,
CoreAddresses
> {
constructor(
public readonly multiProvider: MultiProtocolProvider<CoreAddresses>,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: ChainMap<CoreAddresses>,
public readonly logger = debug('hyperlane:MultiProtocolCore'),
) {
super(multiProvider, logger);
super(multiProvider, addresses, logger);
}
static fromEnvironment<Env extends HyperlaneEnvironment>(
@ -42,15 +43,15 @@ export class MultiProtocolCore extends MultiProtocolApp<
addressesMap: ChainMap<CoreAddresses>,
multiProvider: MultiProtocolProvider,
): MultiProtocolCore {
const mpWithAddresses = multiProvider
.intersect(Object.keys(addressesMap))
.result.extendChainMetadata(addressesMap);
return new MultiProtocolCore(mpWithAddresses);
return new MultiProtocolCore(
multiProvider.intersect(Object.keys(addressesMap)).result,
addressesMap,
);
}
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<CoreAddresses, ICoreAdapter> {
): AdapterClassType<ICoreAdapter> {
if (protocol === ProtocolType.Ethereum) return EvmCoreAdapter;
if (protocol === ProtocolType.Sealevel) return SealevelCoreAdapter;
throw new Error(`No adapter for protocol ${protocol}`);

@ -1,16 +1,8 @@
import {
Address,
HexString,
ProtocolType,
objMap,
pick,
} from '@hyperlane-xyz/utils';
import { Mailbox__factory } from '@hyperlane-xyz/core';
import { Address, HexString } from '@hyperlane-xyz/utils';
import { BaseEvmAdapter } from '../../app/MultiProtocolApp';
import {
attachContractsMap,
filterAddressesToProtocol,
} from '../../contracts/contracts';
import { HyperlaneContractsMap } from '../../contracts/types';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import {
ProviderType,
@ -18,40 +10,30 @@ import {
} from '../../providers/ProviderType';
import { ChainName } from '../../types';
import { HyperlaneCore } from '../HyperlaneCore';
import { CoreAddresses, coreFactories } from '../contracts';
import { CoreFactories } from '../contracts';
import { ICoreAdapter } from './types';
// Explicitly omit timelockController b.c. most chains don't have it in SDK artifacts
type CoreAddressKeys = keyof Omit<CoreAddresses, 'timelockController'>;
// This adapter just routes to the HyperlaneCore
// Which implements the needed functionality for EVM chains
export class EvmCoreAdapter
extends BaseEvmAdapter<CoreAddresses>
implements ICoreAdapter
{
// TODO deprecate HyperlaneCore and replace all evm-specific classes with adapters
export class EvmCoreAdapter extends BaseEvmAdapter implements ICoreAdapter {
core: HyperlaneCore;
constructor(
public readonly multiProvider: MultiProtocolProvider<CoreAddresses>,
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { mailbox: Address },
) {
super(multiProvider);
// Pick out the addresses from the metadata in the multiProvider
// Reminder: MultiProtocol Apps expect the addresses to be in the metadata
const contractNames = Object.keys(coreFactories) as Array<CoreAddressKeys>;
const addresses = objMap(multiProvider.metadata, (_, m) =>
pick<CoreAddressKeys, Address>(m, contractNames),
);
// Then filter it to just the addresses for Ethereum chains
// Otherwise the factory creators will throw
const filteredAddresses = filterAddressesToProtocol(
addresses,
ProtocolType.Ethereum,
multiProvider,
);
const contractsMap = attachContractsMap(filteredAddresses, coreFactories);
super(chainName, multiProvider, addresses);
const contractsMap = {
[chainName]: {
mailbox: Mailbox__factory.connect(
addresses.mailbox,
multiProvider.getEthersV5Provider(chainName),
),
},
} as HyperlaneContractsMap<CoreFactories>; // Core only uses mailbox so cast to keep adapter interface simple
this.core = new HyperlaneCore(
contractsMap,
multiProvider.toMultiProvider(),

@ -1,14 +1,14 @@
import { PublicKey } from '@solana/web3.js';
import { HexString, pollAsync } from '@hyperlane-xyz/utils';
import { Address, HexString, pollAsync } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import {
ProviderType,
TypedTransactionReceipt,
} from '../../providers/ProviderType';
import { ChainName } from '../../types';
import { CoreAddresses } from '../contracts';
import { ICoreAdapter } from './types';
@ -16,9 +16,17 @@ import { ICoreAdapter } from './types';
const MESSAGE_DISPATCH_LOG_REGEX = /Dispatched message to (.*), ID (.*)/;
export class SealevelCoreAdapter
extends BaseSealevelAdapter<CoreAddresses>
extends BaseSealevelAdapter
implements ICoreAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { mailbox: Address },
) {
super(chainName, multiProvider, addresses);
}
extractMessageIds(
sourceTx: TypedTransactionReceipt,
): Array<{ messageId: HexString; destination: ChainName }> {
@ -45,10 +53,8 @@ export class SealevelCoreAdapter
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
const destinationMailbox =
this.multiProvider.getChainMetadata(destination).mailbox;
const pda = SealevelCoreAdapter.deriveMailboxMessageProcessedPda(
destinationMailbox,
this.addresses.mailbox,
messageId,
);
const connection = this.multiProvider.getSolanaWeb3Provider(destination);

@ -3,9 +3,8 @@ import type { HexString } from '@hyperlane-xyz/utils';
import type { BaseAppAdapter } from '../../app/MultiProtocolApp';
import type { TypedTransactionReceipt } from '../../providers/ProviderType';
import type { ChainName } from '../../types';
import type { CoreAddresses } from '../contracts';
export interface ICoreAdapter extends BaseAppAdapter<CoreAddresses> {
export interface ICoreAdapter extends BaseAppAdapter {
extractMessageIds(
r: TypedTransactionReceipt,
): Array<{ messageId: HexString; destination: ChainName }>;

@ -13,11 +13,11 @@ export const coreFactories = {
timelockController: new TimelockController__factory(),
};
export interface CoreAddresses {
export type CoreAddresses = {
validatorAnnounce: Address;
proxyAdmin: Address;
mailbox: Address;
timelockController?: Address;
}
};
export type CoreFactories = typeof coreFactories;

@ -0,0 +1,58 @@
import { PublicKey } from '@solana/web3.js';
import { deserializeUnchecked } from 'borsh';
import { Address } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import { ChainName } from '../../types';
import { SealevelAccountDataWrapper } from '../../utils/sealevelSerialization';
import {
SealevelOverheadIgpData,
SealevelOverheadIgpDataSchema,
} from './serialization';
export class SealevelOverheadIgpAdapter extends BaseSealevelAdapter {
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { igp: Address },
) {
super(chainName, multiProvider, addresses);
}
async getAccountInfo(): Promise<SealevelOverheadIgpData> {
const address = this.addresses.igp;
const connection = this.getProvider();
const accountInfo = await connection.getAccountInfo(new PublicKey(address));
if (!accountInfo) throw new Error(`No account info found for ${address}}`);
const accountData = deserializeUnchecked(
SealevelOverheadIgpDataSchema,
SealevelAccountDataWrapper,
accountInfo.data,
);
return accountData.data as SealevelOverheadIgpData;
}
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/pda_seeds.rs#L7
static deriveIgpProgramPda(igpProgramId: string | PublicKey): PublicKey {
return super.derivePda(
['hyperlane_igp', '-', 'program_data'],
igpProgramId,
);
}
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/pda_seeds.rs#L62
static deriveGasPaymentPda(
igpProgramId: string | PublicKey,
randomWalletPubKey: PublicKey,
): PublicKey {
return super.derivePda(
['hyperlane_igp', '-', 'gas_payment', '-', randomWalletPubKey.toBuffer()],
igpProgramId,
);
}
}

@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { PublicKey } from '@solana/web3.js';
import { Domain } from '@hyperlane-xyz/utils';
import {
SealevelAccountDataWrapper,
getSealevelAccountDataSchema,
} from '../../utils/sealevelSerialization';
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs#L24
export enum SealevelInterchainGasPaymasterType {
// An IGP with gas oracles and that receives lamports as payment.
Igp = 0,
// An overhead IGP that points to an inner IGP and imposes a gas overhead for each destination domain.
OverheadIgp = 1,
}
/**
* IGP Config Borsh Schema
*/
// Config schema, e.g. for use in token data
export class SealevelInterchainGasPaymasterConfig {
program_id!: Uint8Array;
program_id_pubkey!: PublicKey;
type!: SealevelInterchainGasPaymasterType;
igp_account?: Uint8Array;
igp_account_pub_key?: PublicKey;
constructor(public readonly fields: any) {
Object.assign(this, fields);
this.program_id_pubkey = new PublicKey(this.program_id);
this.igp_account_pub_key = this.igp_account
? new PublicKey(this.igp_account)
: undefined;
}
}
export const SealevelInterchainGasPaymasterConfigSchema = {
kind: 'struct',
fields: [
['program_id', [32]],
['type', 'u8'],
['igp_account', [32]],
],
};
/**
* IGP Program Data Borsh Schema
*/
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs#L91
export class SealevelOverheadIgpData {
/// The bump seed for this PDA.
bump!: number;
/// The salt used to derive the overhead IGP PDA.
salt!: Uint8Array;
/// The owner of the overhead IGP.
owner?: Uint8Array;
owner_pub_key?: PublicKey;
/// The inner IGP account.
inner!: Uint8Array;
inner_pub_key!: PublicKey;
/// The gas overheads to impose on gas payments to each destination domain.
gas_overheads!: Map<Domain, bigint>;
constructor(public readonly fields: any) {
Object.assign(this, fields);
this.owner_pub_key = this.owner ? new PublicKey(this.owner) : undefined;
this.inner_pub_key = new PublicKey(this.inner);
}
}
export const SealevelOverheadIgpDataSchema = new Map<any, any>([
[
SealevelAccountDataWrapper,
getSealevelAccountDataSchema(SealevelOverheadIgpData, [8]),
],
[
SealevelOverheadIgpData,
{
kind: 'struct',
fields: [
['bump', 'u8'],
['salt', [32]],
['owner', { kind: 'option', type: [32] }],
['inner', [32]],
['gas_overheads', { kind: 'map', key: 'u32', value: 'u64' }],
],
},
],
]);

@ -10,6 +10,7 @@ export {
chainIdToMetadata,
chainMetadata,
mainnetChainsMetadata,
solanaChainToClusterName,
testnetChainsMetadata,
} from './consts/chainMetadata';
export {
@ -87,6 +88,14 @@ export * as verificationUtils from './deploy/verify/utils';
export { HyperlaneIgp } from './gas/HyperlaneIgp';
export { HyperlaneIgpChecker } from './gas/HyperlaneIgpChecker';
export { HyperlaneIgpDeployer } from './gas/HyperlaneIgpDeployer';
export { SealevelOverheadIgpAdapter } from './gas/adapters/SealevelIgpAdapter';
export {
SealevelInterchainGasPaymasterConfig,
SealevelInterchainGasPaymasterConfigSchema,
SealevelInterchainGasPaymasterType,
SealevelOverheadIgpData,
SealevelOverheadIgpDataSchema,
} from './gas/adapters/serialization';
export { IgpFactories, igpFactories } from './gas/contracts';
export { CoinGeckoTokenPriceGetter } from './gas/token-prices';
export {
@ -253,18 +262,6 @@ 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,
@ -279,4 +276,9 @@ export {
export { MultiGeneric } from './utils/MultiGeneric';
export { filterByChains } from './utils/filter';
export { multisigIsmVerificationCost } from './utils/ism';
export {
SealevelAccountDataWrapper,
SealevelInstructionWrapper,
getSealevelAccountDataSchema,
} from './utils/sealevelSerialization';
export { chainMetadataToWagmiChain, wagmiChainMetadata } from './utils/wagmi';

@ -1,8 +1,11 @@
import { Debugger, debug } from 'debug';
import { exclude, isNumeric, pick } from '@hyperlane-xyz/utils';
import { ProtocolType, exclude, isNumeric, pick } from '@hyperlane-xyz/utils';
import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata';
import {
chainMetadata as defaultChainMetadata,
solanaChainToClusterName,
} from '../consts/chainMetadata';
import { ChainMap, ChainName } from '../types';
import {
@ -210,9 +213,17 @@ export class ChainMetadataManager<MetaExt = {}> {
* Get a block explorer URL for a given chain name, chain id, or domain id
*/
tryGetExplorerUrl(chainNameOrId: ChainName | number): string | null {
const explorers = this.tryGetChainMetadata(chainNameOrId)?.blockExplorers;
if (!explorers?.length) return null;
return explorers[0].url;
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata?.blockExplorers?.length) return null;
const url = new URL(metadata.blockExplorers[0].url);
// TODO move handling of these chain/protocol specific quirks to ChainMetadata
if (
metadata.protocol === ProtocolType.Sealevel &&
solanaChainToClusterName[metadata.name]
) {
url.searchParams.set('cluster', solanaChainToClusterName[metadata.name]);
}
return url.toString();
}
/**
@ -229,9 +240,11 @@ export class ChainMetadataManager<MetaExt = {}> {
* Get a block explorer's API URL for a given chain name, chain id, or domain id
*/
tryGetExplorerApiUrl(chainNameOrId: ChainName | number): string | null {
const explorers = this.tryGetChainMetadata(chainNameOrId)?.blockExplorers;
if (!explorers?.length || !explorers[0].apiUrl) return null;
const { apiUrl, apiKey } = explorers[0];
const metadata = this.tryGetChainMetadata(chainNameOrId);
const { protocol, blockExplorers } = metadata || {};
if (protocol !== ProtocolType.Ethereum) return null;
if (!blockExplorers?.length || !blockExplorers[0].apiUrl) return null;
const { apiUrl, apiKey } = blockExplorers[0];
if (!apiKey) return apiUrl;
const url = new URL(apiUrl);
url.searchParams.set('apikey', apiKey);
@ -256,7 +269,43 @@ export class ChainMetadataManager<MetaExt = {}> {
response: { hash: string },
): string | null {
const baseUrl = this.tryGetExplorerUrl(chainNameOrId);
return baseUrl ? `${baseUrl}/tx/${response.hash}` : null;
if (!baseUrl) return null;
const chainName = this.getChainName(chainNameOrId);
const urlPathStub = ['nautilus', 'proteustestnet'].includes(chainName)
? 'transaction'
: 'tx';
const url = new URL(baseUrl);
url.pathname += `/${urlPathStub}/${response.hash}`;
return url.toString();
}
/**
* Get a block explorer URL for given chain's address
*/
async tryGetExplorerAddressUrl(
chainNameOrId: ChainName | number,
address?: string,
): Promise<string | null> {
if (!address) return null;
const baseUrl = this.tryGetExplorerUrl(chainNameOrId);
if (!baseUrl) return null;
const url = new URL(baseUrl);
url.pathname += `/address/${address}`;
return url.toString();
}
/**
* Get a block explorer URL for given chain's address
* @throws if address or the chain's block explorer data has no been set
*/
async getExplorerAddressUrl(
chainNameOrId: ChainName | number,
address?: string,
): Promise<string> {
const url = await this.tryGetExplorerAddressUrl(chainNameOrId, address);
if (!url)
throw new Error(`Missing data for address url for ${chainNameOrId}`);
return url;
}
/**

@ -1,6 +1,6 @@
import { Debugger, debug } from 'debug';
import { objFilter, objMap, pick } from '@hyperlane-xyz/utils';
import { ProtocolType, objFilter, objMap, pick } from '@hyperlane-xyz/utils';
import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata';
import { ChainMetadataManager } from '../metadata/ChainMetadataManager';
@ -21,6 +21,13 @@ import {
defaultProviderBuilderMap,
} from './providerBuilders';
export const PROTOCOL_DEFAULT_PROVIDER_TYPE: Partial<
Record<ProtocolType, ProviderType>
> = {
[ProtocolType.Ethereum]: ProviderType.EthersV5,
[ProtocolType.Sealevel]: ProviderType.SolanaWeb3,
};
export interface MultiProtocolProviderOptions {
loggerName?: string;
providers?: ChainMap<ProviderMap<TypedProvider>>;
@ -107,11 +114,13 @@ export class MultiProtocolProvider<
tryGetProvider(
chainNameOrId: ChainName | number,
type: ProviderType,
type?: ProviderType,
): TypedProvider | null {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata) return null;
const { name, chainId, rpcUrls } = metadata;
const { protocol, name, chainId, rpcUrls } = metadata;
type = type || PROTOCOL_DEFAULT_PROVIDER_TYPE[protocol];
if (!type) return null;
if (this.providers[name]?.[type]) return this.providers[name][type]!;
@ -126,7 +135,7 @@ export class MultiProtocolProvider<
getProvider(
chainNameOrId: ChainName | number,
type: ProviderType,
type?: ProviderType,
): TypedProvider {
const provider = this.tryGetProvider(chainNameOrId, type);
if (!provider)

@ -264,30 +264,17 @@ export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
/**
* Get a block explorer URL for given chain's address
*/
async tryGetExplorerAddressUrl(
override async tryGetExplorerAddressUrl(
chainNameOrId: ChainName | number,
address?: string,
): Promise<string | null> {
const baseUrl = this.tryGetExplorerUrl(chainNameOrId);
if (!baseUrl) return null;
if (address) return `${baseUrl}/address/${address}`;
if (address) return super.tryGetExplorerAddressUrl(chainNameOrId, address);
const signer = this.tryGetSigner(chainNameOrId);
if (!signer) return null;
return `${baseUrl}/address/${await signer.getAddress()}`;
}
/**
* Get a block explorer URL for given chain's address
* @throws if chain's metadata, signer, or block explorer data has no been set
*/
async getExplorerAddressUrl(
chainNameOrId: ChainName | number,
address?: string,
): Promise<string> {
const url = await this.tryGetExplorerAddressUrl(chainNameOrId, address);
if (!url)
throw new Error(`Missing data for address url for ${chainNameOrId}`);
return url;
if (signer) {
const signerAddr = await signer.getAddress();
return super.tryGetExplorerAddressUrl(chainNameOrId, signerAddr);
}
return null;
}
/**

@ -1,4 +1,5 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import { Chains } from '../consts/chains';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
@ -11,7 +12,9 @@ describe('MultiProtocolRouterApp', () => {
describe('constructs', () => {
const multiProvider = new MultiProtocolProvider<RouterAddress>();
it('creates an app class', async () => {
const app = new MultiProtocolRouterApp(multiProvider);
const app = new MultiProtocolRouterApp(multiProvider, {
ethereum: { router: ethers.constants.AddressZero },
});
expect(app).to.be.instanceOf(MultiProtocolRouterApp);
const ethAdapter = app.adapter(Chains.ethereum);
expect(ethAdapter).to.be.instanceOf(EvmRouterAdapter);

@ -17,12 +17,12 @@ import { RouterAddress } from './types';
export { Router } from '@hyperlane-xyz/core';
export class MultiProtocolRouterApp<
ContractAddrs extends RouterAddress = RouterAddress,
IAdapterApi extends IRouterAdapter = IRouterAdapter,
> extends MultiProtocolApp<ContractAddrs, IAdapterApi> {
ContractAddrs extends RouterAddress = RouterAddress,
> extends MultiProtocolApp<IAdapterApi, ContractAddrs> {
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi> {
): AdapterClassType<IAdapterApi> {
// Casts are required here to allow for default adapters while still
// enabling extensible generic types
if (protocol === ProtocolType.Ethereum) return EvmRouterAdapter as any;
@ -31,33 +31,31 @@ export class MultiProtocolRouterApp<
}
router(chain: ChainName): Address {
return this.metadata(chain).router;
return this.addresses[chain].router;
}
interchainSecurityModules(): Promise<ChainMap<Address>> {
return this.adapterMap((chain, adapter) =>
adapter.interchainSecurityModule(chain),
);
return this.adapterMap((_, adapter) => adapter.interchainSecurityModule());
}
owners(): Promise<ChainMap<Address>> {
return this.adapterMap((chain, adapter) => adapter.owner(chain));
return this.adapterMap((_, adapter) => adapter.owner());
}
remoteRouters(
origin: ChainName,
): Promise<Array<{ domain: Domain; address: Address }>> {
return this.adapter(origin).remoteRouters(origin);
return this.adapter(origin).remoteRouters();
}
}
export class MultiProtocolGasRouterApp<
ContractAddrs extends RouterAddress = RouterAddress,
IAdapterApi extends IGasRouterAdapter = IGasRouterAdapter,
> extends MultiProtocolRouterApp<ContractAddrs, IAdapterApi> {
ContractAddrs extends RouterAddress = RouterAddress,
> extends MultiProtocolRouterApp<IAdapterApi, ContractAddrs> {
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi> {
): AdapterClassType<IAdapterApi> {
// Casts are required here to allow for default adapters while still
// enabling extensible generic types
if (protocol === ProtocolType.Ethereum) return EvmGasRouterAdapter as any;
@ -70,6 +68,6 @@ export class MultiProtocolGasRouterApp<
origin: ChainName,
destination: ChainName,
): Promise<string> {
return this.adapter(origin).quoteGasPayment(origin, destination);
return this.adapter(origin).quoteGasPayment(destination);
}
}

@ -7,76 +7,68 @@ import {
import { Address, Domain, bytes32ToAddress } from '@hyperlane-xyz/utils';
import { BaseEvmAdapter } from '../../app/MultiProtocolApp';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import { ChainName } from '../../types';
import { RouterAddress } from '../types';
import { IGasRouterAdapter, IRouterAdapter } from './types';
export class EvmRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
>
extends BaseEvmAdapter<ContractAddrs>
implements IRouterAdapter<ContractAddrs>
{
interchainSecurityModule(chain: ChainName): Promise<Address> {
return this.getConnectedContract(chain).interchainSecurityModule();
export class EvmRouterAdapter extends BaseEvmAdapter implements IRouterAdapter {
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider<any>,
public readonly addresses: { router: Address },
) {
super(chainName, multiProvider, addresses);
}
interchainSecurityModule(): Promise<Address> {
return this.getConnectedContract().interchainSecurityModule();
}
owner(chain: ChainName): Promise<Address> {
return this.getConnectedContract(chain).owner();
owner(): Promise<Address> {
return this.getConnectedContract().owner();
}
remoteDomains(originChain: ChainName): Promise<Domain[]> {
return this.getConnectedContract(originChain).domains();
remoteDomains(): Promise<Domain[]> {
return this.getConnectedContract().domains();
}
async remoteRouter(
originChain: ChainName,
remoteDomain: Domain,
): Promise<Address> {
const routerAddressesAsBytes32 = await this.getConnectedContract(
originChain,
).routers(remoteDomain);
async remoteRouter(remoteDomain: Domain): Promise<Address> {
const routerAddressesAsBytes32 = await this.getConnectedContract().routers(
remoteDomain,
);
return bytes32ToAddress(routerAddressesAsBytes32);
}
async remoteRouters(
originChain: ChainName,
): Promise<Array<{ domain: Domain; address: Address }>> {
const domains = await this.remoteDomains(originChain);
async remoteRouters(): Promise<Array<{ domain: Domain; address: Address }>> {
const domains = await this.remoteDomains();
const routers: Address[] = await Promise.all(
domains.map((d) => this.remoteRouter(originChain, d)),
domains.map((d) => this.remoteRouter(d)),
);
return domains.map((d, i) => ({ domain: d, address: routers[i] }));
}
getConnectedContract(chain: ChainName): Router {
const address = this.multiProvider.getChainMetadata(chain).router;
const provider = this.multiProvider.getEthersV5Provider(chain);
return Router__factory.connect(address, provider);
getConnectedContract(): Router {
return Router__factory.connect(this.addresses.router, this.getProvider());
}
}
export class EvmGasRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
>
extends EvmRouterAdapter<ContractAddrs>
implements IGasRouterAdapter<ContractAddrs>
export class EvmGasRouterAdapter
extends EvmRouterAdapter
implements IGasRouterAdapter
{
async quoteGasPayment(
origin: ChainName,
destination: ChainName,
): Promise<string> {
async quoteGasPayment(destination: ChainName): Promise<string> {
const destDomain = this.multiProvider.getDomainId(destination);
const amount = await this.getConnectedContract(origin).quoteGasPayment(
const amount = await this.getConnectedContract().quoteGasPayment(
destDomain,
);
return amount.toString();
}
override getConnectedContract(chain: ChainName): GasRouter {
const address = this.multiProvider.getChainMetadata(chain).router;
const provider = this.multiProvider.getEthersV5Provider(chain);
return GasRouter__factory.connect(address, provider);
override getConnectedContract(): GasRouter {
return GasRouter__factory.connect(
this.addresses.router,
this.getProvider(),
);
}
}

@ -1,32 +0,0 @@
import { deserializeUnchecked } from 'borsh';
import { expect } from 'chai';
import { SealevelAccountDataWrapper } from '../../sealevel/serialization';
import {
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
} from '../../sealevel/tokenSerialization';
// Copied from the warp token router program on Solana devnet
const RAW_ACCOUNT_INFO =
'01ff3a280e8466d26bc4e1a5d3d17e73f7b307c082156dd0ffbf8c5f9ae75506d6f14aed87b9d3a2bb5effdbdcd1af363555ff8b6c1311a93c495e6bc722284d2574fb0612012cbc3cc37a2d2e8aaa301fac7e032fbe5d3140f8a12d7445e7fc69f80f60105800000200000061000000a009010000000000c2570100e0ab000000000000020000006100000000000000000000000000000031b5234a896fbc4b3e2f7237592d054716762131c257010000000000000000000000000034a9af13c5555bad0783c220911b9ef59cfdbcef06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9e92839550965ffd4d64acaaf46d45df7318e5b4f57c90c487d60625d829b837b256d8b6f7c1f678a52ef123ddc35c248fcc1e1895e5b8c6d5e6dd381f8090a48fffe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
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(
SealevelHyperlaneTokenDataSchema,
SealevelAccountDataWrapper,
rawData,
);
expect(wrappedData.initialized).to.eql(1);
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);
});
});
});

@ -1,50 +1,46 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { PublicKey } from '@solana/web3.js';
import { deserializeUnchecked } from 'borsh';
import { Address, Domain } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import { SealevelAccountDataWrapper } from '../../sealevel/serialization';
import {
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
} from '../../sealevel/tokenSerialization';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import { ChainName } from '../../types';
import { RouterAddress } from '../types';
import { IGasRouterAdapter, IRouterAdapter } from './types';
export class SealevelRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
>
extends BaseSealevelAdapter<ContractAddrs>
implements IRouterAdapter<ContractAddrs>
export class SealevelRouterAdapter
extends BaseSealevelAdapter
implements IRouterAdapter
{
async interchainSecurityModule(chain: ChainName): Promise<Address> {
const routerAccountInfo = await this.getRouterAccountInfo(chain);
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider<any>,
public readonly addresses: { router: Address },
) {
super(chainName, multiProvider, addresses);
}
async interchainSecurityModule(): Promise<Address> {
const routerAccountInfo = await this.getRouterAccountInfo();
if (!routerAccountInfo.interchain_security_module_pubkey)
throw new Error(`No ism found for router on ${chain}`);
throw new Error(`No ism found for router on ${this.chainName}`);
return routerAccountInfo.interchain_security_module_pubkey.toBase58();
}
async owner(chain: ChainName): Promise<Address> {
const routerAccountInfo = await this.getRouterAccountInfo(chain);
async owner(): Promise<Address> {
const routerAccountInfo = await this.getRouterAccountInfo();
if (!routerAccountInfo.owner_pub_key)
throw new Error(`No owner found for router on ${chain}`);
throw new Error(`No owner found for router on ${this.chainName}`);
return routerAccountInfo.owner_pub_key.toBase58();
}
async remoteDomains(originChain: ChainName): Promise<Domain[]> {
const routers = await this.remoteRouters(originChain);
async remoteDomains(): Promise<Domain[]> {
const routers = await this.remoteRouters();
return routers.map((router) => router.domain);
}
async remoteRouter(
originChain: ChainName,
remoteDomain: Domain,
): Promise<Address> {
const routers = await this.remoteRouters(originChain);
async remoteRouter(remoteDomain: Domain): Promise<Address> {
const routers = await this.remoteRouters();
const addr = routers.find(
(router) => router.domain === remoteDomain,
)?.address;
@ -52,10 +48,8 @@ export class SealevelRouterAdapter<
return addr;
}
async remoteRouters(
originChain: ChainName,
): Promise<Array<{ domain: Domain; address: Address }>> {
const routerAccountInfo = await this.getRouterAccountInfo(originChain);
async remoteRouters(): Promise<Array<{ domain: Domain; address: Address }>> {
const routerAccountInfo = await this.getRouterAccountInfo();
const domainToPubKey = routerAccountInfo.remote_router_pubkeys;
return Array.from(domainToPubKey.entries()).map(([domain, pubKey]) => ({
domain,
@ -63,54 +57,29 @@ 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<SealevelHyperlaneTokenData> {
const address = this.multiProvider.getChainMetadata(chain).router;
const connection = this.multiProvider.getSolanaWeb3Provider(chain);
const msgRecipientPda = this.deriveMessageRecipientPda(address);
const accountInfo = await connection.getAccountInfo(msgRecipientPda);
if (!accountInfo)
throw new Error(
`No account info found for ${msgRecipientPda.toBase58()}}`,
);
const accountData = deserializeUnchecked(
SealevelHyperlaneTokenDataSchema,
SealevelAccountDataWrapper,
accountInfo.data,
);
return accountData.data as SealevelHyperlaneTokenData;
getRouterAccountInfo(): Promise<{
owner_pub_key?: PublicKey;
interchain_security_module?: Uint8Array;
interchain_security_module_pubkey?: PublicKey;
remote_router_pubkeys: Map<Domain, PublicKey>;
}> {
throw new Error('TODO getRouterAccountInfo not yet implemented');
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs
deriveMessageRecipientPda(routerAddress: Address | PublicKey): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
[
Buffer.from('hyperlane_message_recipient'),
Buffer.from('-'),
Buffer.from('handle'),
Buffer.from('-'),
Buffer.from('account_metas'),
],
new PublicKey(routerAddress),
return super.derivePda(
['hyperlane_message_recipient', '-', 'handle', '-', 'account_metas'],
routerAddress,
);
return pda;
}
}
export class SealevelGasRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
>
extends SealevelRouterAdapter<ContractAddrs>
implements IGasRouterAdapter<ContractAddrs>
export class SealevelGasRouterAdapter
extends SealevelRouterAdapter
implements IGasRouterAdapter
{
async quoteGasPayment(
_origin: ChainName,
_destination: ChainName,
): Promise<string> {
async quoteGasPayment(_destination: ChainName): Promise<string> {
throw new Error('Gas payments not yet supported for sealevel');
}
}

@ -2,28 +2,15 @@ import { Address, Domain } from '@hyperlane-xyz/utils';
import { BaseAppAdapter } from '../../app/MultiProtocolApp';
import { ChainName } from '../../types';
import { RouterAddress } from '../types';
export interface IRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
> extends BaseAppAdapter<ContractAddrs> {
interchainSecurityModule(chain: ChainName): Promise<Address>;
owner: (chain: ChainName) => Promise<Address>;
remoteDomains(originChain: ChainName): Promise<Domain[]>;
remoteRouter: (
originChain: ChainName,
remoteDomain: Domain,
) => Promise<Address>;
remoteRouters: (
originChain: ChainName,
) => Promise<Array<{ domain: Domain; address: Address }>>;
export interface IRouterAdapter extends BaseAppAdapter {
interchainSecurityModule(): Promise<Address>;
owner: () => Promise<Address>;
remoteDomains(): Promise<Domain[]>;
remoteRouter: (remoteDomain: Domain) => Promise<Address>;
remoteRouters: () => Promise<Array<{ domain: Domain; address: Address }>>;
}
export interface IGasRouterAdapter<
ContractAddrs extends RouterAddress = RouterAddress,
> extends IRouterAdapter<ContractAddrs> {
quoteGasPayment: (
origin: ChainName,
destination: ChainName,
) => Promise<string>;
export interface IGasRouterAdapter extends IRouterAdapter {
quoteGasPayment: (destination: ChainName) => Promise<string>;
}

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
export class SealevelInstructionWrapper<Instr> {
instruction!: number;
data!: Instr;
@ -9,6 +8,7 @@ export class SealevelInstructionWrapper<Instr> {
export class SealevelAccountDataWrapper<T> {
initialized!: boolean;
discriminator?: unknown;
data!: T;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
constructor(public readonly fields: any) {
@ -16,11 +16,15 @@ export class SealevelAccountDataWrapper<T> {
}
}
export function getSealevelAccountDataSchema<T>(DataClass: T) {
export function getSealevelAccountDataSchema<T>(
DataClass: T,
discriminator?: any,
) {
return {
kind: 'struct',
fields: [
['initialized', 'u8'],
...(discriminator ? [['discriminator', discriminator]] : []),
['data', DataClass],
],
};

@ -0,0 +1,212 @@
import { BigNumber, PopulatedTransaction } from 'ethers';
import {
BaseEvmAdapter,
ChainName,
MultiProtocolProvider,
} from '@hyperlane-xyz/sdk';
import {
Address,
Domain,
addressToByteHexString,
addressToBytes32,
bytes32ToAddress,
strip0x,
} from '@hyperlane-xyz/utils';
import { MinimalTokenMetadata } from '../config';
import {
ERC20,
ERC20__factory,
HypERC20,
HypERC20Collateral__factory,
HypERC20__factory,
} from '../types';
import {
IHypTokenAdapter,
ITokenAdapter,
TransferParams,
TransferRemoteParams,
} from './ITokenAdapter';
// Interacts with native currencies
export class EvmNativeTokenAdapter
extends BaseEvmAdapter
implements ITokenAdapter
{
async getBalance(address: Address): Promise<string> {
const balance = await this.getProvider().getBalance(address);
return balance.toString();
}
async getMetadata(): Promise<MinimalTokenMetadata> {
// TODO get metadata from chainMetadata config
throw new Error('Metadata not available to native tokens');
}
async populateApproveTx(
_params: TransferParams,
): Promise<PopulatedTransaction> {
throw new Error('Approve not required for native tokens');
}
async populateTransferTx({
weiAmountOrId,
recipient,
}: TransferParams): Promise<PopulatedTransaction> {
const value = BigNumber.from(weiAmountOrId);
return { value, to: recipient };
}
}
// Interacts with ERC20/721 contracts
export class EvmTokenAdapter<T extends ERC20 = ERC20>
extends EvmNativeTokenAdapter
implements ITokenAdapter
{
public readonly contract: T;
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { token: Address },
public readonly contractFactory: any = ERC20__factory,
) {
super(chainName, multiProvider, addresses);
this.contract = contractFactory.connect(
addresses.token,
this.getProvider(),
);
}
override async getBalance(address: Address): Promise<string> {
const balance = await this.contract.balanceOf(address);
return balance.toString();
}
override async getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata> {
const [decimals, symbol, name] = await Promise.all([
isNft ? 0 : this.contract.decimals(),
this.contract.symbol(),
this.contract.name(),
]);
return { decimals, symbol, name };
}
override populateApproveTx({
weiAmountOrId,
recipient,
}: TransferParams): Promise<PopulatedTransaction> {
return this.contract.populateTransaction.approve(recipient, weiAmountOrId);
}
override populateTransferTx({
weiAmountOrId,
recipient,
}: TransferParams): Promise<PopulatedTransaction> {
return this.contract.populateTransaction.transfer(recipient, weiAmountOrId);
}
}
// Interacts with Hyp Synthetic token contracts (aka 'HypTokens')
export class EvmHypSyntheticAdapter<T extends HypERC20 = HypERC20>
extends EvmTokenAdapter<T>
implements IHypTokenAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { token: Address },
public readonly contractFactory: any = HypERC20__factory,
) {
super(chainName, multiProvider, addresses, contractFactory);
}
getDomains(): Promise<Domain[]> {
return this.contract.domains();
}
async getRouterAddress(domain: Domain): Promise<Buffer> {
const routerAddressesAsBytes32 = await this.contract.routers(domain);
// Evm addresses will be padded with 12 bytes
if (routerAddressesAsBytes32.startsWith('0x000000000000000000000000')) {
return Buffer.from(
strip0x(bytes32ToAddress(routerAddressesAsBytes32)),
'hex',
);
// Otherwise leave the address unchanged
} else {
return Buffer.from(strip0x(routerAddressesAsBytes32), 'hex');
}
}
async getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>> {
const domains = await this.getDomains();
const routers: Buffer[] = await Promise.all(
domains.map((d) => this.getRouterAddress(d)),
);
return domains.map((d, i) => ({ domain: d, address: routers[i] }));
}
async quoteGasPayment(destination: Domain): Promise<string> {
const gasPayment = await this.contract.quoteGasPayment(destination);
return gasPayment.toString();
}
populateTransferRemoteTx({
weiAmountOrId,
destination,
recipient,
txValue,
}: TransferRemoteParams): Promise<PopulatedTransaction> {
const recipBytes32 = addressToBytes32(addressToByteHexString(recipient));
return this.contract.populateTransaction.transferRemote(
destination,
recipBytes32,
weiAmountOrId,
{
// Note, typically the value is the gas payment as quoted by IGP
value: txValue,
},
);
}
}
// Interacts with HypCollateral and HypNative contracts
export class EvmHypCollateralAdapter
extends EvmHypSyntheticAdapter
implements IHypTokenAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { token: Address },
public readonly contractFactory: any = HypERC20Collateral__factory,
) {
super(chainName, multiProvider, addresses, contractFactory);
}
override getMetadata(): Promise<MinimalTokenMetadata> {
// TODO pass through metadata from wrapped token or chainMetadata config
throw new Error(
'Metadata not available for HypCollateral/HypNative contract.',
);
}
override populateApproveTx(
_params: TransferParams,
): Promise<PopulatedTransaction> {
throw new Error(
'Approve not applicable to HypCollateral/HypNative contract.',
);
}
override populateTransferTx(
_params: TransferParams,
): Promise<PopulatedTransaction> {
throw new Error(
'Local transfer not supported for HypCollateral/HypNative contract.',
);
}
}

@ -1,8 +1,6 @@
import { Address, Domain } from '@hyperlane-xyz/utils';
import { ERC20Metadata } from '../config';
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply'>;
import { MinimalTokenMetadata } from '../config';
export interface TransferParams {
weiAmountOrId: string | number;
@ -12,7 +10,6 @@ export interface TransferParams {
// Included here optionally to keep Adapter types simple
fromTokenAccount?: Address;
fromAccountOwner?: Address;
mailbox?: Address;
}
export interface TransferRemoteParams extends TransferParams {
@ -21,7 +18,7 @@ export interface TransferRemoteParams extends TransferParams {
}
export interface ITokenAdapter {
getBalance(address?: Address): Promise<string>;
getBalance(address: Address): Promise<string>;
getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata>;
populateApproveTx(TransferParams: TransferParams): unknown | Promise<unknown>;
populateTransferTx(

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
@ -7,7 +6,6 @@ import {
} from '@solana/spl-token';
import {
AccountMeta,
Connection,
Keypair,
PublicKey,
SystemProgram,
@ -18,41 +16,47 @@ import BigNumber from 'bignumber.js';
import { deserializeUnchecked, serialize } from 'borsh';
import {
BaseSealevelAdapter,
ChainName,
MultiProtocolProvider,
SEALEVEL_SPL_NOOP_ADDRESS,
SealevelAccountDataWrapper,
SealevelHypTokenInstruction,
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
SealevelInstructionWrapper,
SealevelTransferRemoteInstruction,
SealevelTransferRemoteSchema,
SealevelInterchainGasPaymasterType,
SealevelOverheadIgpAdapter,
} from '@hyperlane-xyz/sdk';
import {
Address,
Domain,
addressToBytes,
eqAddress,
isZeroishAddress,
} from '@hyperlane-xyz/utils';
import { MinimalTokenMetadata } from '../config';
import {
IHypTokenAdapter,
ITokenAdapter,
MinimalTokenMetadata,
TransferParams,
TransferRemoteParams,
} from './ITokenAdapter';
import {
SealevelHypTokenInstruction,
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
SealevelTransferRemoteInstruction,
SealevelTransferRemoteSchema,
} from './serialization';
// 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);
export class SealevelNativeTokenAdapter
extends BaseSealevelAdapter
implements ITokenAdapter
{
async getBalance(address: Address): Promise<string> {
const balance = await this.getProvider().getBalance(new PublicKey(address));
return balance.toString();
}
@ -69,10 +73,11 @@ export class SealevelNativeTokenAdapter implements ITokenAdapter {
recipient,
fromAccountOwner,
}: TransferParams): Transaction {
const fromPubkey = resolveAddress(fromAccountOwner, this.signerAddress);
if (!fromAccountOwner)
throw new Error('fromAccountOwner required for Sealevel');
return new Transaction().add(
SystemProgram.transfer({
fromPubkey,
fromPubkey: new PublicKey(fromAccountOwner),
toPubkey: new PublicKey(recipient),
lamports: new BigNumber(weiAmountOrId).toNumber(),
}),
@ -81,25 +86,31 @@ export class SealevelNativeTokenAdapter implements ITokenAdapter {
}
// Interacts with SPL token programs
export class SealevelTokenAdapter implements ITokenAdapter {
export class SealevelTokenAdapter
extends BaseSealevelAdapter
implements ITokenAdapter
{
public readonly tokenProgramPubKey: PublicKey;
constructor(
public readonly connection: Connection,
public readonly tokenProgramId: Address,
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { token: Address },
public readonly isSpl2022: boolean = false,
public readonly signerAddress?: Address,
) {
this.tokenProgramPubKey = new PublicKey(tokenProgramId);
super(chainName, multiProvider, addresses);
this.tokenProgramPubKey = new PublicKey(addresses.token);
}
async getBalance(owner: Address): Promise<string> {
const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner));
const response = await this.connection.getTokenAccountBalance(tokenPubKey);
const response = await this.getProvider().getTokenAccountBalance(
tokenPubKey,
);
return response.value.amount;
}
async getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata> {
async getMetadata(_isNft?: boolean): Promise<MinimalTokenMetadata> {
// TODO solana support
return { decimals: 9, symbol: 'SPL', name: 'SPL Token' };
}
@ -114,16 +125,15 @@ export class SealevelTokenAdapter implements ITokenAdapter {
fromAccountOwner,
fromTokenAccount,
}: TransferParams): Transaction {
if (!fromTokenAccount) throw new Error('No fromTokenAccount provided');
const fromWalletPubKey = resolveAddress(
fromAccountOwner,
this.signerAddress,
);
if (!fromTokenAccount)
throw new Error('fromTokenAccount required for Sealevel');
if (!fromAccountOwner)
throw new Error('fromAccountOwner required for Sealevel');
return new Transaction().add(
createTransferInstruction(
new PublicKey(fromTokenAccount),
new PublicKey(recipient),
fromWalletPubKey,
new PublicKey(fromAccountOwner),
new BigNumber(weiAmountOrId).toNumber(),
),
);
@ -148,32 +158,41 @@ export abstract class SealevelHypTokenAdapter
implements IHypTokenAdapter
{
public readonly warpProgramPubKey: PublicKey;
protected cachedTokenAccountData: SealevelHyperlaneTokenData | undefined;
constructor(
public readonly connection: Connection,
public readonly warpRouteProgramId: Address,
public readonly tokenProgramId: Address,
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: {
token: Address;
warpRouter: Address;
mailbox: 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)
const superTokenProgramId = isZeroishAddress(addresses.token)
? SystemProgram.programId.toBase58()
: tokenProgramId;
super(connection, superTokenProgramId, isSpl2022, signerAddress);
this.warpProgramPubKey = new PublicKey(warpRouteProgramId);
: addresses.token;
super(chainName, multiProvider, { token: superTokenProgramId }, isSpl2022);
this.warpProgramPubKey = new PublicKey(addresses.warpRouter);
}
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;
if (!this.cachedTokenAccountData) {
const tokenPda = this.deriveHypTokenAccount();
const accountInfo = await this.getProvider().getAccountInfo(tokenPda);
if (!accountInfo)
throw new Error(`No account info found for ${tokenPda}`);
const wrappedData = deserializeUnchecked(
SealevelHyperlaneTokenDataSchema,
SealevelAccountDataWrapper,
accountInfo.data,
);
this.cachedTokenAccountData =
wrappedData.data as SealevelHyperlaneTokenData;
}
return this.cachedTokenAccountData;
}
override async getMetadata(): Promise<MinimalTokenMetadata> {
@ -207,7 +226,7 @@ export abstract class SealevelHypTokenAdapter
}));
}
async quoteGasPayment(destination: Domain): Promise<string> {
async quoteGasPayment(_destination: Domain): Promise<string> {
// TODO Solana support
return '0';
}
@ -217,20 +236,19 @@ export abstract class SealevelHypTokenAdapter
destination,
recipient,
fromAccountOwner,
mailbox,
}: TransferRemoteParams): Promise<Transaction> {
if (!mailbox) throw new Error('No mailbox provided');
const fromWalletPubKey = resolveAddress(
fromAccountOwner,
this.signerAddress,
);
if (!fromAccountOwner)
throw new Error('fromAccountOwner required for Sealevel');
const randomWallet = Keypair.generate();
const mailboxPubKey = new PublicKey(mailbox);
const keys = this.getTransferInstructionKeyList(
fromWalletPubKey,
mailboxPubKey,
randomWallet.publicKey,
);
const fromWalletPubKey = new PublicKey(fromAccountOwner);
const mailboxPubKey = new PublicKey(this.addresses.mailbox);
const keys = this.getTransferInstructionKeyList({
sender: fromWalletPubKey,
mailbox: mailboxPubKey,
randomWallet: randomWallet.publicKey,
igp: await this.getIgpKeys(),
});
const value = new SealevelInstructionWrapper({
instruction: SealevelHypTokenInstruction.TransferRemote,
@ -254,7 +272,7 @@ export abstract class SealevelHypTokenAdapter
});
const recentBlockhash = (
await this.connection.getLatestBlockhash('finalized')
await this.getProvider().getLatestBlockhash('finalized')
).blockhash;
// @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash
const tx = new Transaction({
@ -266,12 +284,44 @@ export abstract class SealevelHypTokenAdapter
return tx;
}
getTransferInstructionKeyList(
sender: PublicKey,
mailbox: PublicKey,
randomWallet: PublicKey,
): Array<AccountMeta> {
return [
async getIgpKeys() {
const tokenData = await this.getTokenAccountData();
if (!tokenData.interchain_gas_paymaster) return undefined;
const igpConfig = tokenData.interchain_gas_paymaster;
if (igpConfig.type === SealevelInterchainGasPaymasterType.Igp) {
return {
programId: igpConfig.program_id_pubkey,
};
} else if (
igpConfig.type === SealevelInterchainGasPaymasterType.OverheadIgp
) {
if (!igpConfig.igp_account_pub_key) {
throw new Error('igpAccount field expected for Sealevel Overhead IGP');
}
const overheadAdapter = new SealevelOverheadIgpAdapter(
this.chainName,
this.multiProvider,
{ igp: igpConfig.igp_account_pub_key.toBase58() },
);
const overheadAccountInfo = await overheadAdapter.getAccountInfo();
return {
programId: igpConfig.program_id_pubkey,
igpAccount: igpConfig.igp_account_pub_key,
innerIgpAccount: overheadAccountInfo.inner_pub_key,
};
} else {
throw new Error(`Unsupported IGP type ${igpConfig.type}`);
}
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs#L257-L274
getTransferInstructionKeyList({
sender,
mailbox,
randomWallet,
igp,
}: KeyListParams): Array<AccountMeta> {
let keys = [
// 0. [executable] The system program.
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
// 1. [executable] The spl_noop program.
@ -305,31 +355,75 @@ export abstract class SealevelHypTokenAdapter
// 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, },
{
pubkey: this.deriveMsgStorageAccount(mailbox, randomWallet),
isSigner: false,
isWritable: true,
},
];
if (igp) {
keys = [
...keys,
// 9. [executable] The IGP program.
{ pubkey: igp.programId, isSigner: false, isWritable: false },
// 10. [writeable] The IGP program data.
{
pubkey: SealevelOverheadIgpAdapter.deriveIgpProgramPda(igp.programId),
isSigner: false,
isWritable: true,
},
// 11. [writeable] Gas payment PDA.
{
pubkey: SealevelOverheadIgpAdapter.deriveGasPaymentPda(
igp.programId,
randomWallet,
),
isSigner: false,
isWritable: true,
},
];
if (igp.igpAccount && igp.innerIgpAccount) {
keys = [
...keys,
// 12. [] OPTIONAL - The Overhead IGP account, if the configured IGP is an Overhead IGP
{
pubkey: igp.igpAccount,
isSigner: false,
isWritable: false,
},
// 13. [writeable] The Overhead's inner IGP account
{
pubkey: igp.innerIgpAccount,
isSigner: false,
isWritable: true,
},
];
} else {
keys = [
...keys,
// 12. [writeable] The IGP account.
{
pubkey: igp.programId,
isSigner: false,
isWritable: true,
},
];
}
}
return keys;
}
// 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;
return super.derivePda(['hyperlane', '-', 'outbox'], mailbox);
}
// 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'),
],
return super.derivePda(
['hyperlane_dispatcher', '-', '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
@ -337,32 +431,24 @@ export abstract class SealevelHypTokenAdapter
mailbox: PublicKey,
randomWalletPubKey: PublicKey,
): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
return super.derivePda(
[
Buffer.from('hyperlane'),
Buffer.from('-'),
Buffer.from('dispatched_message'),
Buffer.from('-'),
'hyperlane',
'-',
'dispatched_message',
'-',
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'),
],
return super.derivePda(
['hyperlane_message_recipient', '-', 'handle', '-', 'account_metas'],
this.warpProgramPubKey,
);
return pda;
}
}
@ -371,22 +457,20 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter {
public readonly wrappedNative: SealevelNativeTokenAdapter;
constructor(
public readonly connection: Connection,
public readonly warpRouteProgramId: Address,
public readonly tokenProgramId: Address,
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: {
token: Address;
warpRouter: Address;
mailbox: Address;
},
public readonly isSpl2022: boolean = false,
public readonly signerAddress?: Address,
) {
super(
connection,
warpRouteProgramId,
tokenProgramId,
isSpl2022,
signerAddress,
);
super(chainName, multiProvider, addresses, isSpl2022);
this.wrappedNative = new SealevelNativeTokenAdapter(
connection,
signerAddress,
chainName,
multiProvider,
{},
);
}
@ -398,13 +482,9 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter {
return this.wrappedNative.getMetadata();
}
getTransferInstructionKeyList(
sender: PublicKey,
mailbox: PublicKey,
randomWallet: PublicKey,
): Array<AccountMeta> {
getTransferInstructionKeyList(params: KeyListParams): Array<AccountMeta> {
return [
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet),
...super.getTransferInstructionKeyList(params),
// 9. [executable] The system program.
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
// 10. [writeable] The native token collateral PDA account.
@ -418,15 +498,10 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter {
// 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'),
],
return super.derivePda(
['hyperlane_token', '-', 'native_collateral'],
this.warpProgramPubKey,
);
return pda;
}
}
@ -437,9 +512,9 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter {
// 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) {
if (eqAddress(owner, this.addresses.warpRouter)) {
const collateralAccount = this.deriveEscrowAccount();
const response = await this.connection.getTokenAccountBalance(
const response = await this.getProvider().getTokenAccountBalance(
collateralAccount,
);
return response.value.amount;
@ -449,19 +524,17 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter {
}
override getTransferInstructionKeyList(
sender: PublicKey,
mailbox: PublicKey,
randomWallet: PublicKey,
params: KeyListParams,
): Array<AccountMeta> {
return [
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet),
...super.getTransferInstructionKeyList(params),
/// 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),
pubkey: this.deriveAssociatedTokenAccount(params.sender),
isSigner: false,
isWritable: true,
},
@ -471,23 +544,20 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter {
}
deriveEscrowAccount(): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('escrow')],
return super.derivePda(
['hyperlane_token', '-', '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,
params: KeyListParams,
): Array<AccountMeta> {
return [
...super.getTransferInstructionKeyList(sender, mailbox, randomWallet),
...super.getTransferInstructionKeyList(params),
/// 9. [executable] The spl_token_2022 program.
{ pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
/// 10. [writeable] The mint / mint authority PDA account.
@ -498,7 +568,7 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter {
},
/// 11. [writeable] The token sender's associated token account, from which tokens will be burned.
{
pubkey: this.deriveAssociatedTokenAccount(sender),
pubkey: this.deriveAssociatedTokenAccount(params.sender),
isSigner: false,
isWritable: true,
},
@ -507,16 +577,17 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter {
override async getBalance(owner: Address): Promise<string> {
const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner));
const response = await this.connection.getTokenAccountBalance(tokenPubKey);
const response = await this.getProvider().getTokenAccountBalance(
tokenPubKey,
);
return response.value.amount;
}
deriveMintAuthorityAccount(): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('mint')],
return super.derivePda(
['hyperlane_token', '-', 'mint'],
this.warpProgramPubKey,
);
return pda;
}
override deriveAssociatedTokenAccount(owner: PublicKey): PublicKey {
@ -529,8 +600,13 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter {
}
}
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');
interface KeyListParams {
sender: PublicKey;
mailbox: PublicKey;
randomWallet: PublicKey;
igp?: {
programId: PublicKey;
igpAccount?: PublicKey;
innerIgpAccount?: PublicKey;
};
}

@ -1,17 +1,13 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { PublicKey } from '@solana/web3.js';
import { Domain } from '@hyperlane-xyz/utils';
import {
SealevelAccountDataWrapper,
SealevelInstructionWrapper,
SealevelInterchainGasPaymasterConfig,
SealevelInterchainGasPaymasterConfigSchema,
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
} from '@hyperlane-xyz/sdk';
import { Domain } from '@hyperlane-xyz/utils';
/**
* Hyperlane Token Borsh Schema
@ -39,11 +35,7 @@ export class SealevelHyperlaneTokenData {
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?: SealevelInterchainGasPaymasterConfig;
interchain_gas_paymaster_pubkey?: PublicKey;
interchain_gas_paymaster_account_pubkey?: PublicKey;
// Gas amounts by destination
@ -64,8 +56,8 @@ export class SealevelHyperlaneTokenData {
? 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)
?.igp_account
? new PublicKey(this.interchain_gas_paymaster.igp_account)
: undefined;
this.remote_router_pubkeys = new Map<number, PublicKey>();
if (this.remote_routers) {
@ -98,14 +90,7 @@ export const SealevelHyperlaneTokenDataSchema = new Map<any, any>([
'interchain_gas_paymaster',
{
kind: 'option',
type: {
kind: 'struct',
fields: [
['program_id', [32]],
['type', 'u8'],
['account', [32]],
],
},
type: SealevelInterchainGasPaymasterConfig,
},
],
['destination_gas', { kind: 'map', key: 'u32', value: 'u64' }],
@ -113,6 +98,10 @@ export const SealevelHyperlaneTokenDataSchema = new Map<any, any>([
],
},
],
[
SealevelInterchainGasPaymasterConfig,
SealevelInterchainGasPaymasterConfigSchema,
],
]);
/**

@ -1,80 +0,0 @@
import { BigNumberish } from 'ethers';
import { ChainName, HyperlaneContracts, RouterApp } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import {
HypERC20Factories,
HypERC721Factories,
TokenFactories,
} from './contracts';
import { TokenRouter } from './types';
class HyperlaneTokenApp<
Factories extends TokenFactories,
> extends RouterApp<Factories> {
router(contracts: HyperlaneContracts<TokenFactories>): TokenRouter {
return contracts.router;
}
async transfer(
origin: ChainName,
destination: ChainName,
recipient: Address,
amountOrId: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const destProvider = this.multiProvider.getProvider(destination);
const destinationNetwork = await destProvider.getNetwork();
const gasPayment = await originRouter.quoteGasPayment(
destinationNetwork.chainId,
);
return this.multiProvider.handleTx(
origin,
originRouter.transferRemote(
destinationNetwork.chainId,
recipient,
amountOrId,
{
value: gasPayment,
},
),
);
}
}
export class HypERC20App extends HyperlaneTokenApp<HypERC20Factories> {
async transfer(
origin: ChainName,
destination: ChainName,
recipient: Address,
amount: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const signerAddress = await this.multiProvider.getSignerAddress(origin);
const balance = await originRouter.balanceOf(signerAddress);
if (balance.lt(amount))
console.warn(
`Signer ${signerAddress} has insufficient balance ${balance}, needs ${amount} on ${origin}`,
);
return super.transfer(origin, destination, recipient, amount);
}
}
export class HypERC721App extends HyperlaneTokenApp<HypERC721Factories> {
async transfer(
origin: ChainName,
destination: ChainName,
recipient: Address,
tokenId: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const signerAddress = await this.multiProvider.getSignerAddress(origin);
const owner = await originRouter.ownerOf(tokenId);
if (signerAddress != owner)
console.warn(
`Signer ${signerAddress} not owner of token ${tokenId} on ${origin}`,
);
return super.transfer(origin, destination, recipient, tokenId);
}
}

@ -22,6 +22,7 @@ export type TokenDecimals = {
};
export type ERC20Metadata = TokenMetadata & TokenDecimals;
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply' | 'scale'>;
export const isTokenMetadata = (metadata: any): metadata is TokenMetadata =>
metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0

@ -1,5 +1,30 @@
export { SealevelHypCollateralAdapter } from './adapters/SealevelTokenAdapter';
export { HypERC20App, HypERC721App } from './app';
export {
EvmHypCollateralAdapter,
EvmHypSyntheticAdapter,
EvmNativeTokenAdapter,
EvmTokenAdapter,
} from './adapters/EvmTokenAdapter';
export {
IHypTokenAdapter,
ITokenAdapter,
TransferParams,
TransferRemoteParams,
} from './adapters/ITokenAdapter';
export {
SealevelHypCollateralAdapter,
SealevelHypNativeAdapter,
SealevelHypSyntheticAdapter,
SealevelHypTokenAdapter,
SealevelNativeTokenAdapter,
SealevelTokenAdapter,
} from './adapters/SealevelTokenAdapter';
export {
SealevelHypTokenInstruction,
SealevelHyperlaneTokenData,
SealevelHyperlaneTokenDataSchema,
SealevelTransferRemoteInstruction,
SealevelTransferRemoteSchema,
} from './adapters/serialization';
export {
CollateralConfig,
ERC20Metadata,
@ -10,6 +35,7 @@ export {
HypERC721CollateralConfig,
HypERC721Config,
HypNativeConfig,
MinimalTokenMetadata,
NativeConfig,
SyntheticConfig,
TokenConfig,

@ -5,6 +5,7 @@ export {
addressToBytesEvm,
addressToBytesSol,
bytes32ToAddress,
bytesToProtocolAddress,
capitalizeAddress,
convertToProtocolAddress,
ensure0x,
@ -28,6 +29,7 @@ export {
strip0x,
} from './src/addresses';
export {
convertDecimals,
eqAmountApproximate,
fromWei,
fromWeiRounded,
@ -48,7 +50,6 @@ export {
BigNumberMax,
BigNumberMin,
bigToFixed,
convertDecimalValue,
fixedToBig,
isBigNumberish,
isZeroish,
@ -96,7 +97,6 @@ export {
Address,
AddressBytes32,
CallData,
ChainCaip19Id,
ChainCaip2Id,
Checkpoint,
Domain,
@ -112,6 +112,7 @@ export {
S3Checkpoint,
S3CheckpointWithId,
SignatureLike,
TokenCaip19Id,
} from './src/types';
export { assert } from './src/validation';
export { BaseValidator, Validator } from './src/validator';

@ -202,6 +202,19 @@ export function addressToByteHexString(
return '0x' + Buffer.from(addressToBytes(address, protocol)).toString('hex');
}
export function bytesToProtocolAddress(
bytes: Buffer,
toProtocol: ProtocolType,
) {
if (toProtocol === ProtocolType.Sealevel) {
return new PublicKey(bytes).toBase58();
} else if (toProtocol === ProtocolType.Ethereum) {
return bytes32ToAddress(bytes.toString('hex'));
} else {
throw new Error(`Unsupported protocol for address ${toProtocol}`);
}
}
export function convertToProtocolAddress(
address: string,
protocol: ProtocolType,

@ -2,7 +2,7 @@ import { formatUnits, parseUnits } from '@ethersproject/units';
import BigNumber from 'bignumber.js';
const DEFAULT_MIN_ROUNDED_VALUE = 0.00001;
const DEFAULT_DISPLAY_DECIMALS = 5;
const DEFAULT_DISPLAY_DECIMALS = 4;
const DEFAULT_TOKEN_DECIMALS = 18;
type NumberT = BigNumber.Value;
@ -35,10 +35,11 @@ export function fromWeiRounded(
// If amount is less than min value
if (amount.lt(DEFAULT_MIN_ROUNDED_VALUE)) {
if (roundDownIfSmall) return '0';
else return DEFAULT_MIN_ROUNDED_VALUE.toString();
return amount.toString(10);
}
return amount.toFixed(DEFAULT_DISPLAY_DECIMALS).toString();
const displayDecimals = amount.gte(10000) ? 2 : DEFAULT_DISPLAY_DECIMALS;
return amount.toFixed(displayDecimals).toString();
}
export function toWei(
@ -46,7 +47,11 @@ export function toWei(
decimals = DEFAULT_TOKEN_DECIMALS,
): BigNumber {
if (!value) return new BigNumber(0);
const valueString = value.toString().trim();
// First convert to a BigNumber, and then call `toString` with the
// explicit radix 10 such that the result is formatted as a base-10 string
// and not in scientific notation.
const valueBN = new BigNumber(value);
const valueString = valueBN.toString(10).trim();
const components = valueString.split('.');
if (components.length === 1) {
return new BigNumber(parseUnits(valueString, decimals).toString());
@ -83,3 +88,32 @@ export function eqAmountApproximate(
// Is difference btwn amount and balance less than min amount shown for token
return amountInWei1.minus(amountInWei2).abs().lt(minValueWei);
}
/**
* Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals.
* Incurs a loss of precision when `fromDecimals` > `toDecimals`.
* @param fromDecimals The number of decimals `value` has.
* @param toDecimals The number of decimals to convert `value` to.
* @param value The value to convert.
* @returns `value` represented with `toDecimals` decimals.
*/
export function convertDecimals(
fromDecimals: number,
toDecimals: number,
value: NumberT,
) {
const amount = new BigNumber(value);
if (fromDecimals === toDecimals) return amount;
else if (fromDecimals > toDecimals) {
const difference = fromDecimals - toDecimals;
return amount
.div(new BigNumber(10).pow(difference))
.integerValue(BigNumber.ROUND_FLOOR);
}
// fromDecimals < toDecimals
else {
const difference = toDecimals - fromDecimals;
return amount.times(new BigNumber(10).pow(difference));
}
}

@ -63,29 +63,6 @@ export function mulBigAndFixed(
return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil);
}
/**
* Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals.
* Incurs a loss of precision when `fromDecimals` > `toDecimals`.
* @param value The value to convert.
* @param fromDecimals The number of decimals `value` has.
* @param toDecimals The number of decimals to convert `value` to.
* @returns `value` represented with `toDecimals` decimals.
*/
export function convertDecimalValue(
value: BigNumber,
fromDecimals: number,
toDecimals: number,
): BigNumber {
if (fromDecimals === toDecimals) {
return value;
} else if (fromDecimals > toDecimals) {
return value.div(10 ** (fromDecimals - toDecimals));
} else {
// if (fromDecimals < toDecimals)
return value.mul(10 ** (toDecimals - fromDecimals));
}
}
export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) {
return bn1.gte(bn2) ? bn2 : bn1;
}

@ -18,7 +18,7 @@ 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 TokenCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f
export type HexString = string;
// copied from node_modules/@ethersproject/bytes/src.ts/index.ts

Loading…
Cancel
Save