Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-papayas-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

feat: Starknet SDK logic integration
1 change: 1 addition & 0 deletions typescript/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@cosmjs/stargate": "^0.32.4",
"@hyperlane-xyz/core": "7.1.5",
"@hyperlane-xyz/cosmos-sdk": "12.6.0",
"@hyperlane-xyz/starknet-core": "1.0.0",
"@hyperlane-xyz/utils": "12.6.0",
"@safe-global/api-kit": "1.3.0",
"@safe-global/protocol-kit": "1.3.0",
Expand Down
9 changes: 9 additions & 0 deletions typescript/sdk/src/app/MultiProtocolApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
CosmJsWasmProvider,
EthersV5Provider,
SolanaWeb3Provider,
StarknetJsProvider,
TypedProvider,
} from '../providers/ProviderType.js';
import { ChainMap, ChainName } from '../types.js';
Expand Down Expand Up @@ -104,6 +105,14 @@ export class BaseSealevelAdapter extends BaseAppAdapter {
}
}

export class BaseStarknetAdapter extends BaseAppAdapter {
public readonly protocol: ProtocolType = ProtocolType.Starknet;

public getProvider(): StarknetJsProvider['provider'] {
return this.multiProvider.getStarknetProvider(this.chainName);
}
}

/**
* A version of HyperlaneApp that can support different
* provider types across different protocol types.
Expand Down
19 changes: 19 additions & 0 deletions typescript/sdk/src/consts/testChains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,32 @@ export const testSealevelChain: ChainMetadata = {
rpcUrls: [{ http: 'http://127.0.0.1:8899' }],
};

export const testStarknetChain: ChainMetadata = {
chainId: '0x534e5f5345504f4c4941',
domainId: 5854809,
name: 'starknetdevnet',
nativeToken: {
decimals: 18,
denom: '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7',
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Universal ETH address on Starknet

name: 'Ether',
symbol: 'ETH',
},
protocol: ProtocolType.Starknet,
rpcUrls: [
{
http: 'http://127.0.0.1:5050',
},
],
};

export const multiProtocolTestChainMetadata: ChainMap<ChainMetadata> = {
...testChainMetadata,
testcosmos: testCosmosChain,
testsealevel: testSealevelChain,
testxerc20: testXERC20,
testvsxerc20: testVSXERC20,
testxerc20lockbox: testXERC20Lockbox,
starknetdevnet: testStarknetChain,
};

export const multiProtocolTestChains: Array<ChainName> = Object.keys(
Expand Down
2 changes: 2 additions & 0 deletions typescript/sdk/src/core/MultiProtocolCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CosmNativeCoreAdapter } from './adapters/CosmNativeCoreAdapter.js';
import { CosmWasmCoreAdapter } from './adapters/CosmWasmCoreAdapter.js';
import { EvmCoreAdapter } from './adapters/EvmCoreAdapter.js';
import { SealevelCoreAdapter } from './adapters/SealevelCoreAdapter.js';
import { StarknetCoreAdapter } from './adapters/StarknetCoreAdapter.js';
import { ICoreAdapter } from './adapters/types.js';
import { CoreAddresses } from './contracts.js';

Expand Down Expand Up @@ -41,6 +42,7 @@ export class MultiProtocolCore extends MultiProtocolApp<
if (protocol === ProtocolType.Sealevel) return SealevelCoreAdapter;
if (protocol === ProtocolType.Cosmos) return CosmWasmCoreAdapter;
if (protocol === ProtocolType.CosmosNative) return CosmNativeCoreAdapter;
if (protocol === ProtocolType.Starknet) return StarknetCoreAdapter;
throw new Error(`No adapter for protocol ${protocol}`);
}

Expand Down
127 changes: 127 additions & 0 deletions typescript/sdk/src/core/adapters/StarknetCoreAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
CallData,
InvokeTransactionReceiptResponse,
ParsedEvents,
events as eventsUtils,
} from 'starknet';

import { getCompiledContract } from '@hyperlane-xyz/starknet-core';
import { Address, HexString, pollAsync } from '@hyperlane-xyz/utils';

import { BaseStarknetAdapter } from '../../app/MultiProtocolApp.js';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js';
import {
ProviderType,
StarknetJsTransactionReceipt,
} from '../../providers/ProviderType.js';
import { ChainName } from '../../types.js';
import {
getStarknetMailboxContract,
parseStarknetDispatchEvents,
} from '../../utils/starknet.js';

import { ICoreAdapter } from './types.js';

export class StarknetCoreAdapter
extends BaseStarknetAdapter
implements ICoreAdapter
{
constructor(
public readonly chainName: ChainName,
public readonly multiProvider: MultiProtocolProvider,
public readonly addresses: { mailbox: Address },
) {
super(chainName, multiProvider, addresses);
}

extractMessageIds(
sourceTx: StarknetJsTransactionReceipt,
): Array<{ messageId: string; destination: ChainName }> {
if (sourceTx.type !== ProviderType.Starknet) {
throw new Error(
`Unsupported provider type for StarknetCoreAdapter ${sourceTx.type}`,
);
}

let parsedEvents: ParsedEvents = [];
sourceTx.receipt.match({
success: (txR) => {
const emittedEvents =
(txR as InvokeTransactionReceiptResponse).events?.map((event) => {
return {
block_hash: (txR as any).block_hash,
block_number: (txR as any).block_number,
transaction_hash: (txR as any).transaction_hash,
...event,
};
}) || [];

if (emittedEvents.length === 0) return;
const mailboxAbi = getCompiledContract('mailbox').abi;
parsedEvents = eventsUtils.parseEvents(
emittedEvents,
eventsUtils.getAbiEvents(mailboxAbi),
CallData.getAbiStruct(mailboxAbi),
CallData.getAbiEnum(mailboxAbi),
);
},
_: () => {
throw Error('This transaction was not successful.');
},
});

if (!parsedEvents || parsedEvents.length === 0) return [];

const messages = parseStarknetDispatchEvents(
parsedEvents,
(domain) => this.multiProvider.tryGetChainName(domain) ?? undefined,
);

return messages.map(({ id, parsed }) => ({
messageId: id,
destination: this.multiProvider.getChainName(parsed.destination),
}));
}

async waitForMessageProcessed(
messageId: HexString,
destination: ChainName,
delayMs = 5000,
maxAttempts = 60,
): Promise<boolean> {
const destAdapter = new StarknetCoreAdapter(
destination,
this.multiProvider,
{ mailbox: this.addresses.mailbox },
);

const mailboxContract = getStarknetMailboxContract(
destAdapter.addresses.mailbox,
destAdapter.getProvider(),
);

await pollAsync(
async () => {
const isDelivered = await mailboxContract.call('delivered', [
messageId,
]);

if (!isDelivered) {
throw new Error(
`Message ${messageId} not yet delivered on ${destination}`,
);
}

this.logger.debug(
`Message ${messageId} confirmed delivered on ${destination}`,
);

return isDelivered;
},
delayMs,
maxAttempts,
);

return true;
}
}
14 changes: 14 additions & 0 deletions typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
export { CosmWasmCoreAdapter } from './core/adapters/CosmWasmCoreAdapter.js';
export { EvmCoreAdapter } from './core/adapters/EvmCoreAdapter.js';
export { SealevelCoreAdapter } from './core/adapters/SealevelCoreAdapter.js';
export { StarknetCoreAdapter } from './core/adapters/StarknetCoreAdapter.js';
export { ICoreAdapter } from './core/adapters/types.js';
export {
CoreAddresses,
Expand Down Expand Up @@ -366,6 +367,10 @@ export {
SolanaWeb3Provider,
SolanaWeb3Transaction,
SolanaWeb3TransactionReceipt,
StarknetJsContract,
StarknetJsProvider,
StarknetJsTransaction,
StarknetJsTransactionReceipt,
TypedContract,
TypedProvider,
TypedTransaction,
Expand Down Expand Up @@ -712,3 +717,12 @@ export {
CCIPContractCache,
} from './ccip/utils.js';
export { HyperlaneCCIPDeployer } from './ccip/HyperlaneCCIPDeployer.js';

export {
StarknetContractName,
getStarknetContract,
getStarknetHypERC20Contract,
getStarknetHypERC20CollateralContract,
getStarknetMailboxContract,
getStarknetEtherContract,
} from './utils/starknet.js';
10 changes: 10 additions & 0 deletions typescript/sdk/src/providers/MultiProtocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ProviderMap,
ProviderType,
SolanaWeb3Provider,
StarknetJsProvider,
TypedProvider,
TypedTransaction,
ViemProvider,
Expand Down Expand Up @@ -215,6 +216,15 @@ export class MultiProtocolProvider<
);
}

getStarknetProvider(
chainNameOrId: ChainNameOrId,
): StarknetJsProvider['provider'] {
return this.getSpecificProvider<StarknetJsProvider['provider']>(
chainNameOrId,
ProviderType.Starknet,
);
}

setProvider(
chainNameOrId: ChainNameOrId,
provider: TypedProvider,
Expand Down
2 changes: 1 addition & 1 deletion typescript/sdk/src/providers/MultiProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
// setup contract factory
const overrides = this.getTransactionOverrides(chainNameOrId);
const signer = this.getSigner(chainNameOrId);
const contractFactory = await factory.connect(signer);
const contractFactory = factory.connect(signer);

// estimate gas
const deployTx = contractFactory.getDeployTransaction(...params);
Expand Down
7 changes: 3 additions & 4 deletions typescript/sdk/src/providers/ProviderType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
Contract as StarknetContract,
Invocation as StarknetInvocation,
Provider as StarknetProvider,
ReceiptTx as StarknetReceiptTx,
TransactionReceipt as StarknetTxReceipt,
GetTransactionReceiptResponse as StarknetTxReceipt,
} from 'starknet';
import type {
GetContractReturnType,
Expand Down Expand Up @@ -356,9 +355,9 @@ export interface CosmJsNativeTransactionReceipt
}

export interface StarknetJsTransactionReceipt
extends TypedTransactionReceiptBase<StarknetTxReceipt | StarknetReceiptTx> {
extends TypedTransactionReceiptBase<StarknetTxReceipt> {
type: ProviderType.Starknet;
receipt: StarknetTxReceipt | StarknetReceiptTx;
receipt: StarknetTxReceipt;
}

export interface ZKSyncTransactionReceipt
Expand Down
13 changes: 13 additions & 0 deletions typescript/sdk/src/providers/rpcHealthTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EthersV5Provider,
ProviderType,
SolanaWeb3Provider,
StarknetJsProvider,
} from './ProviderType.js';
import { protocolToDefaultProviderBuilder } from './providerBuilders.js';

Expand All @@ -30,6 +31,8 @@ export async function isRpcHealthy(
provider.type === ProviderType.CosmJsNative
)
return isCosmJsProviderHealthy(provider.provider, metadata);
else if (provider.type === ProviderType.Starknet)
return isStarknetJsProviderHealthy(provider.provider, metadata);
else
throw new Error(
`Unsupported provider type ${provider.type}, new health check required`,
Expand Down Expand Up @@ -88,3 +91,13 @@ export async function isCosmJsProviderHealthy(
rootLogger.debug(`Block number is okay for ${metadata.name}`);
return true;
}

export async function isStarknetJsProviderHealthy(
provider: StarknetJsProvider['provider'],
metadata: ChainMetadata,
): Promise<boolean> {
const blockNumber = await provider.getBlockNumber();
if (!blockNumber || blockNumber < 0) return false;
rootLogger.debug(`Block number is okay for ${metadata.name}`);
return true;
}
21 changes: 21 additions & 0 deletions typescript/sdk/src/providers/transactionFeeEstimators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
ProviderType,
SolanaWeb3Provider,
SolanaWeb3Transaction,
StarknetJsProvider,
StarknetJsTransaction,
TypedProvider,
TypedTransaction,
ViemProvider,
Expand Down Expand Up @@ -326,9 +328,28 @@ export function estimateTransactionFee({
sender,
senderPubKey,
});
} else if (
transaction.type === ProviderType.Starknet &&
provider.type === ProviderType.Starknet
) {
return estimateTransactionFeeStarknet({ transaction, provider, sender });
} else {
throw new Error(
`Unsupported transaction type ${transaction.type} or provider type ${provider.type} for gas estimation`,
);
}
}

// Starknet does not support gas estimation without starknet account
// TODO: Figure out a way to inject starknet account
export async function estimateTransactionFeeStarknet({
transaction: _transaction,
provider: _provider,
sender: _sender,
}: {
transaction: StarknetJsTransaction;
provider: StarknetJsProvider;
sender: Address;
}): Promise<TransactionFeeEstimate> {
return { gasUnits: 0, gasPrice: 0, fee: 0 };
}
Loading