Skip to content

Commit

Permalink
ntt_solana_watcher: implement solana ntt watcher
Browse files Browse the repository at this point in the history
Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: update psql feature

Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: add comments

Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: cleanup

Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: fix gh issues

Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: delete key when transfer is complete

Signed-off-by: bingyuyap <[email protected]>

ntt_solana_watcher: format

Signed-off-by: bingyuyap <[email protected]>
  • Loading branch information
bingyuyap authored and panoel committed Mar 28, 2024
1 parent e1951ec commit 029aa7f
Show file tree
Hide file tree
Showing 24 changed files with 8,858 additions and 416 deletions.
11 changes: 2 additions & 9 deletions common/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type Network = {
logo: string;
type: 'guardian' | 'cloudfunction';
};
export type Mode = 'vaa' | 'ntt';

export const INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN: {
[key in Environment]: { [key in ChainName]?: string };
Expand Down Expand Up @@ -106,7 +107,7 @@ export const INITIAL_NTT_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN: {
} = {
['mainnet']: {},
['testnet']: {
solana: '284788472',
solana: '285100152',
sepolia: '5472203',
arbitrum_sepolia: '22501243',
base_sepolia: '7249669',
Expand Down Expand Up @@ -192,14 +193,6 @@ export const CIRCLE_DOMAIN_TO_CHAIN_ID: { [key: number]: ChainId } = {
7: CHAIN_ID_POLYGON,
};

// TODO: This should be needed by processVaa.ts, if we go down that path
export const NTT_EMITTERS: { [key in ChainName]?: string } = {
// TODO: add NTT emitters
};

export const isNTTEmitter = (chain: ChainId | ChainName, emitter: string) =>
NTT_EMITTERS[coalesceChainName(chain)]?.toLowerCase() === emitter.toLowerCase();

export type CHAIN_INFO = {
name: string;
evm: boolean;
Expand Down
84 changes: 84 additions & 0 deletions common/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import {
Message,
MessageCompiledInstruction,
MessageV0,
VersionedBlockResponse,
SolanaJSONRPCError,
PublicKey,
PublicKeyInitData,
} from '@solana/web3.js';
import { decode } from 'bs58';
import { Connection } from '@solana/web3.js';
import { RPCS_BY_CHAIN } from '@certusone/wormhole-sdk/lib/cjs/relayer';
import { CONTRACTS } from '@certusone/wormhole-sdk';
import { encoding } from '@wormhole-foundation/sdk-base';
import { BN } from '@coral-xyz/anchor';

export const isLegacyMessage = (message: Message | MessageV0): message is Message => {
return message.version === 'legacy';
Expand Down Expand Up @@ -60,3 +66,81 @@ export async function convertSolanaTxToAccts(txHash: string): Promise<string[]>
}
return accounts;
}

export const findNextValidBlock = async (
connection: Connection,
slot: number,
next: number,
retries: number
): Promise<VersionedBlockResponse> => {
// identify block range by fetching signatures of the first and last transactions
// getSignaturesForAddress walks backwards so fromSignature occurs after toSignature
if (retries === 0) throw new Error(`No block found after exhausting retries`);

let block: VersionedBlockResponse | null = null;
try {
block = await connection.getBlock(slot, { maxSupportedTransactionVersion: 0 });
} catch (e) {
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
// failed to get confirmed block: slot was skipped or missing in long-term storage
return findNextValidBlock(connection, slot + next, next, retries - 1);
} else {
throw e;
}
}

if (!block || !block.blockTime || block.transactions.length === 0) {
return findNextValidBlock(connection, slot + next, next, retries - 1);
}

return block;
};

export const findFromSignatureAndToSignature = async (
connection: Connection,
fromSlot: number,
toSlot: number,
retries = 5
) => {
let toBlock: VersionedBlockResponse;
let fromBlock: VersionedBlockResponse;

try {
toBlock = await findNextValidBlock(connection, toSlot + 1, -1, retries);
fromBlock = await findNextValidBlock(connection, fromSlot - 1, 1, retries);
} catch (e) {
throw new Error('solana: invalid block range: ' + (e as Error).message);
}

const fromSignature = toBlock.transactions[0].transaction.signatures[0];
const toSignature =
fromBlock.transactions[fromBlock.transactions.length - 1].transaction.signatures[0];

return { fromSignature, toSignature, toBlock };
};

// copied from https://github.com/wormhole-foundation/example-native-token-transfers/blob/main/solana/ts/sdk/utils.ts#L38-L52
export const U64 = {
MAX: new BN((2n ** 64n - 1n).toString()),
to: (amount: number, unit: number) => {
const ret = new BN(Math.round(amount * unit));

if (ret.isNeg()) throw new Error('Value negative');

if (ret.bitLength() > 64) throw new Error('Value too large');

return ret;
},
from: (amount: BN, unit: number) => amount.toNumber() / unit,
};

// copied from https://github.com/wormhole-foundation/example-native-token-transfers/blob/main/solana/ts/sdk/utils.ts#L55-L56
type Seed = Uint8Array | string;
export function derivePda(seeds: Seed | readonly Seed[], programId: PublicKeyInitData) {
const toBytes = (s: string | Uint8Array) =>
typeof s === 'string' ? encoding.bytes.encode(s) : s;
return PublicKey.findProgramAddressSync(
Array.isArray(seeds) ? seeds.map(toBytes) : [toBytes(seeds as Seed)],
new PublicKey(programId)
)[0];
}
10 changes: 9 additions & 1 deletion common/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Environment } from './consts';
import { Environment, Mode } from './consts';

export async function sleep(timeout: number) {
return new Promise((resolve) => setTimeout(resolve, timeout));
Expand All @@ -23,3 +23,11 @@ export function getEnvironment(): Environment {
}
throw new Error(`Unknown network: ${network}`);
}

export function getMode(): Mode {
const mode: string = assertEnvironmentVariable('MODE').toLowerCase();
if (mode === 'vaa' || mode === 'ntt') {
return mode;
}
throw new Error(`Unknown mode: ${mode}`);
}
26 changes: 25 additions & 1 deletion database/ntt-lifecycle-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,38 @@ CREATE TABLE life_cycle (
from_token VARCHAR(96),
token_amount DECIMAL(78, 0),
transfer_sent_txhash VARCHAR(96),
transfer_block_height BIGINT,
redeemed_txhash VARCHAR(96),
redeemed_block_height BIGINT,
ntt_transfer_key VARCHAR(256),
vaa_id VARCHAR(128),
digest VARCHAR(96) NOT NULL,
is_relay BOOLEAN,
transfer_time TIMESTAMP,
redeem_time TIMESTAMP,
inbound_transfer_queued_time TIMESTAMP,
outbound_transfer_queued_time TIMESTAMP,
outbound_transfer_rate_limited_time TIMESTAMP,
outbound_transfer_releasable_time TIMESTAMP,
PRIMARY KEY (digest)
);

-- This is needed since releaseInboundMint/releaseInboundUnlock does not reference the digest
-- The redeem stage refers to both digest and inboxItem. Since inboxItem is unique for every transfer
-- we can use it as a primary key.
-- Row will be deleted when the transfer is fully redeemed, aka releaseInboundMint/releaseInboundUnlock is called.
CREATE TABLE inbox_item_to_lifecycle_digest (
inbox_item VARCHAR(96) NOT NULL,
digest VARCHAR(96) NOT NULL,
PRIMARY KEY (inbox_item)
);

-- This is needed since requestRelay does not reference the digest
-- The transfer stage refers to both digest and outboxItem. Since outboxItem is unique for every transfer
-- we can use it as a primary key.
-- Row will be deleted when the requestRelay is executed or when receiveWormhole is called.
-- We will truly know if the transfer is relayed when the transfer reaches the dest chain.
CREATE TABLE outbox_item_to_lifecycle_digest (
outbox_item VARCHAR(96) NOT NULL,
digest VARCHAR(96) NOT NULL,
PRIMARY KEY (outbox_item)
);
Loading

0 comments on commit 029aa7f

Please sign in to comment.