Skip to content

Commit 029aa7f

Browse files
bingyuyappanoel
authored andcommitted
ntt_solana_watcher: implement solana ntt watcher
Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: update psql feature Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: add comments Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: cleanup Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: fix gh issues Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: delete key when transfer is complete Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> ntt_solana_watcher: format Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com>
1 parent e1951ec commit 029aa7f

24 files changed

+8858
-416
lines changed

common/src/consts.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type Network = {
3939
logo: string;
4040
type: 'guardian' | 'cloudfunction';
4141
};
42+
export type Mode = 'vaa' | 'ntt';
4243

4344
export const INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN: {
4445
[key in Environment]: { [key in ChainName]?: string };
@@ -106,7 +107,7 @@ export const INITIAL_NTT_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN: {
106107
} = {
107108
['mainnet']: {},
108109
['testnet']: {
109-
solana: '284788472',
110+
solana: '285100152',
110111
sepolia: '5472203',
111112
arbitrum_sepolia: '22501243',
112113
base_sepolia: '7249669',
@@ -192,14 +193,6 @@ export const CIRCLE_DOMAIN_TO_CHAIN_ID: { [key: number]: ChainId } = {
192193
7: CHAIN_ID_POLYGON,
193194
};
194195

195-
// TODO: This should be needed by processVaa.ts, if we go down that path
196-
export const NTT_EMITTERS: { [key in ChainName]?: string } = {
197-
// TODO: add NTT emitters
198-
};
199-
200-
export const isNTTEmitter = (chain: ChainId | ChainName, emitter: string) =>
201-
NTT_EMITTERS[coalesceChainName(chain)]?.toLowerCase() === emitter.toLowerCase();
202-
203196
export type CHAIN_INFO = {
204197
name: string;
205198
evm: boolean;

common/src/solana.ts

+84
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import {
33
Message,
44
MessageCompiledInstruction,
55
MessageV0,
6+
VersionedBlockResponse,
7+
SolanaJSONRPCError,
8+
PublicKey,
9+
PublicKeyInitData,
610
} from '@solana/web3.js';
711
import { decode } from 'bs58';
812
import { Connection } from '@solana/web3.js';
913
import { RPCS_BY_CHAIN } from '@certusone/wormhole-sdk/lib/cjs/relayer';
1014
import { CONTRACTS } from '@certusone/wormhole-sdk';
15+
import { encoding } from '@wormhole-foundation/sdk-base';
16+
import { BN } from '@coral-xyz/anchor';
1117

1218
export const isLegacyMessage = (message: Message | MessageV0): message is Message => {
1319
return message.version === 'legacy';
@@ -60,3 +66,81 @@ export async function convertSolanaTxToAccts(txHash: string): Promise<string[]>
6066
}
6167
return accounts;
6268
}
69+
70+
export const findNextValidBlock = async (
71+
connection: Connection,
72+
slot: number,
73+
next: number,
74+
retries: number
75+
): Promise<VersionedBlockResponse> => {
76+
// identify block range by fetching signatures of the first and last transactions
77+
// getSignaturesForAddress walks backwards so fromSignature occurs after toSignature
78+
if (retries === 0) throw new Error(`No block found after exhausting retries`);
79+
80+
let block: VersionedBlockResponse | null = null;
81+
try {
82+
block = await connection.getBlock(slot, { maxSupportedTransactionVersion: 0 });
83+
} catch (e) {
84+
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
85+
// failed to get confirmed block: slot was skipped or missing in long-term storage
86+
return findNextValidBlock(connection, slot + next, next, retries - 1);
87+
} else {
88+
throw e;
89+
}
90+
}
91+
92+
if (!block || !block.blockTime || block.transactions.length === 0) {
93+
return findNextValidBlock(connection, slot + next, next, retries - 1);
94+
}
95+
96+
return block;
97+
};
98+
99+
export const findFromSignatureAndToSignature = async (
100+
connection: Connection,
101+
fromSlot: number,
102+
toSlot: number,
103+
retries = 5
104+
) => {
105+
let toBlock: VersionedBlockResponse;
106+
let fromBlock: VersionedBlockResponse;
107+
108+
try {
109+
toBlock = await findNextValidBlock(connection, toSlot + 1, -1, retries);
110+
fromBlock = await findNextValidBlock(connection, fromSlot - 1, 1, retries);
111+
} catch (e) {
112+
throw new Error('solana: invalid block range: ' + (e as Error).message);
113+
}
114+
115+
const fromSignature = toBlock.transactions[0].transaction.signatures[0];
116+
const toSignature =
117+
fromBlock.transactions[fromBlock.transactions.length - 1].transaction.signatures[0];
118+
119+
return { fromSignature, toSignature, toBlock };
120+
};
121+
122+
// copied from https://github.com/wormhole-foundation/example-native-token-transfers/blob/main/solana/ts/sdk/utils.ts#L38-L52
123+
export const U64 = {
124+
MAX: new BN((2n ** 64n - 1n).toString()),
125+
to: (amount: number, unit: number) => {
126+
const ret = new BN(Math.round(amount * unit));
127+
128+
if (ret.isNeg()) throw new Error('Value negative');
129+
130+
if (ret.bitLength() > 64) throw new Error('Value too large');
131+
132+
return ret;
133+
},
134+
from: (amount: BN, unit: number) => amount.toNumber() / unit,
135+
};
136+
137+
// copied from https://github.com/wormhole-foundation/example-native-token-transfers/blob/main/solana/ts/sdk/utils.ts#L55-L56
138+
type Seed = Uint8Array | string;
139+
export function derivePda(seeds: Seed | readonly Seed[], programId: PublicKeyInitData) {
140+
const toBytes = (s: string | Uint8Array) =>
141+
typeof s === 'string' ? encoding.bytes.encode(s) : s;
142+
return PublicKey.findProgramAddressSync(
143+
Array.isArray(seeds) ? seeds.map(toBytes) : [toBytes(seeds as Seed)],
144+
new PublicKey(programId)
145+
)[0];
146+
}

common/src/utils.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Environment } from './consts';
1+
import { Environment, Mode } from './consts';
22

33
export async function sleep(timeout: number) {
44
return new Promise((resolve) => setTimeout(resolve, timeout));
@@ -23,3 +23,11 @@ export function getEnvironment(): Environment {
2323
}
2424
throw new Error(`Unknown network: ${network}`);
2525
}
26+
27+
export function getMode(): Mode {
28+
const mode: string = assertEnvironmentVariable('MODE').toLowerCase();
29+
if (mode === 'vaa' || mode === 'ntt') {
30+
return mode;
31+
}
32+
throw new Error(`Unknown mode: ${mode}`);
33+
}

database/ntt-lifecycle-schema.sql

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,38 @@ CREATE TABLE life_cycle (
55
from_token VARCHAR(96),
66
token_amount DECIMAL(78, 0),
77
transfer_sent_txhash VARCHAR(96),
8+
transfer_block_height BIGINT,
89
redeemed_txhash VARCHAR(96),
10+
redeemed_block_height BIGINT,
911
ntt_transfer_key VARCHAR(256),
1012
vaa_id VARCHAR(128),
1113
digest VARCHAR(96) NOT NULL,
14+
is_relay BOOLEAN,
1215
transfer_time TIMESTAMP,
1316
redeem_time TIMESTAMP,
1417
inbound_transfer_queued_time TIMESTAMP,
1518
outbound_transfer_queued_time TIMESTAMP,
16-
outbound_transfer_rate_limited_time TIMESTAMP,
19+
outbound_transfer_releasable_time TIMESTAMP,
1720
PRIMARY KEY (digest)
1821
);
22+
23+
-- This is needed since releaseInboundMint/releaseInboundUnlock does not reference the digest
24+
-- The redeem stage refers to both digest and inboxItem. Since inboxItem is unique for every transfer
25+
-- we can use it as a primary key.
26+
-- Row will be deleted when the transfer is fully redeemed, aka releaseInboundMint/releaseInboundUnlock is called.
27+
CREATE TABLE inbox_item_to_lifecycle_digest (
28+
inbox_item VARCHAR(96) NOT NULL,
29+
digest VARCHAR(96) NOT NULL,
30+
PRIMARY KEY (inbox_item)
31+
);
32+
33+
-- This is needed since requestRelay does not reference the digest
34+
-- The transfer stage refers to both digest and outboxItem. Since outboxItem is unique for every transfer
35+
-- we can use it as a primary key.
36+
-- Row will be deleted when the requestRelay is executed or when receiveWormhole is called.
37+
-- We will truly know if the transfer is relayed when the transfer reaches the dest chain.
38+
CREATE TABLE outbox_item_to_lifecycle_digest (
39+
outbox_item VARCHAR(96) NOT NULL,
40+
digest VARCHAR(96) NOT NULL,
41+
PRIMARY KEY (outbox_item)
42+
);

0 commit comments

Comments
 (0)