@mayanfinance/blink-sdk 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -1
- package/dist/contracts.d.ts +40 -0
- package/dist/contracts.js +160 -0
- package/dist/deposit.d.ts +7 -5
- package/dist/deposit.js +95 -64
- package/dist/gen/blink/signer/v1/signer_pb.d.ts +763 -0
- package/dist/gen/blink/signer/v1/signer_pb.js +182 -0
- package/dist/gen/blink/v1/admin_pb.d.ts +590 -20
- package/dist/gen/blink/v1/admin_pb.js +132 -37
- package/dist/gen/blink/v1/api_pb.d.ts +119 -8
- package/dist/gen/blink/v1/api_pb.js +32 -12
- package/dist/gen/blink/v1/partner_pb.d.ts +118 -0
- package/dist/gen/blink/v1/partner_pb.js +46 -21
- package/dist/gen/hular/v1/admin_pb.d.ts +1220 -0
- package/dist/gen/hular/v1/admin_pb.js +219 -0
- package/dist/gen/hular/v1/api_pb.d.ts +1755 -0
- package/dist/gen/hular/v1/api_pb.js +284 -0
- package/dist/gen/hular/v1/partner_pb.d.ts +1020 -0
- package/dist/gen/hular/v1/partner_pb.js +247 -0
- package/dist/gen/hular/v1/ratelimit_pb.d.ts +10 -0
- package/dist/gen/hular/v1/ratelimit_pb.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/order.d.ts +9 -5
- package/dist/order.js +34 -26
- package/dist/sui.d.ts +10 -21
- package/dist/sui.js +55 -168
- package/dist/svm.d.ts +2 -4
- package/dist/svm.js +187 -203
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,3 +13,16 @@ const { blink } = createBlinkClient({ apiKey: process.env.BLINK_API_KEY });
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The client talks to `https://api.hular.dev` unless `baseUrl` is passed. Full documentation is at https://docs.hular.dev.
|
|
16
|
+
|
|
17
|
+
## Contract registry
|
|
18
|
+
|
|
19
|
+
Every contract address a deposit touches is checked against a signed manifest served by `GetContracts`. The SDK pins the genesis guardian set, verifies each later set was approved by a majority of the previous one, and accepts the manifest only when every member of the latest set has signed it. `client.contracts` caches the manifest for two hours, refetches when a quote references a newer guardian set or an address outside the cache, and throws `UnregisteredAddressError` if the address is still unknown.
|
|
20
|
+
|
|
21
|
+
Guardians sign with `scripts/sign-contracts.mjs`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
CONTRACTS_SIGNER_KEY=0x... node scripts/sign-contracts.mjs manifest ../config/contracts.json
|
|
25
|
+
CONTRACTS_SIGNER_KEY=0x... node scripts/sign-contracts.mjs guardians ../config/guardians.json 1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The first form writes the signature next to the manifest in `contracts.sig.json`; the second approves the guardian set with that index on behalf of a member of the previous set.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Client } from "@connectrpc/connect";
|
|
2
|
+
import { type ContractRegistry } from "./contracts.js";
|
|
2
3
|
import { BlinkApi } from "./gen/blink/v1/api_pb.js";
|
|
3
4
|
import { AdminApi } from "./gen/blink/v1/admin_pb.js";
|
|
4
5
|
import { PartnerApi } from "./gen/blink/v1/partner_pb.js";
|
|
@@ -13,5 +14,6 @@ export interface BlinkClient {
|
|
|
13
14
|
blink: Client<typeof BlinkApi>;
|
|
14
15
|
partner: Client<typeof PartnerApi>;
|
|
15
16
|
admin: Client<typeof AdminApi>;
|
|
17
|
+
contracts: ContractRegistry;
|
|
16
18
|
}
|
|
17
19
|
export declare const createBlinkClient: ({ baseUrl, apiKey, token, telemetry, }: BlinkClientOptions) => BlinkClient;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createClient } from "@connectrpc/connect";
|
|
2
2
|
import { createGrpcWebTransport } from "@connectrpc/connect-web";
|
|
3
|
+
import { ApiContractRegistry } from "./contracts.js";
|
|
3
4
|
import { BlinkApi } from "./gen/blink/v1/api_pb.js";
|
|
4
5
|
import { AdminApi } from "./gen/blink/v1/admin_pb.js";
|
|
5
6
|
import { PartnerApi } from "./gen/blink/v1/partner_pb.js";
|
|
@@ -24,9 +25,11 @@ export const createBlinkClient = ({ baseUrl = BLINK_API_URL, apiKey, token, tele
|
|
|
24
25
|
baseUrl,
|
|
25
26
|
interceptors: [headers],
|
|
26
27
|
});
|
|
28
|
+
const blink = createClient(BlinkApi, transport);
|
|
27
29
|
return {
|
|
28
|
-
blink
|
|
30
|
+
blink,
|
|
29
31
|
partner: createClient(PartnerApi, transport),
|
|
30
32
|
admin: createClient(AdminApi, transport),
|
|
33
|
+
contracts: new ApiContractRegistry(blink),
|
|
31
34
|
};
|
|
32
35
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Client } from "@connectrpc/connect";
|
|
2
|
+
import type { BlinkApi, ContractsResponse, GuardianSet } from "./gen/blink/v1/api_pb.js";
|
|
3
|
+
export declare const GENESIS_GUARDIANS: readonly string[];
|
|
4
|
+
export declare const CONTRACTS_REFRESH_MS: number;
|
|
5
|
+
export interface ChainContracts {
|
|
6
|
+
router: string;
|
|
7
|
+
escrow?: string;
|
|
8
|
+
swapPrograms: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ContractManifest {
|
|
11
|
+
guardianSetIndex: number;
|
|
12
|
+
chains: Record<string, ChainContracts>;
|
|
13
|
+
}
|
|
14
|
+
export declare class UnregisteredAddressError extends Error {
|
|
15
|
+
readonly chain: string;
|
|
16
|
+
readonly address: string;
|
|
17
|
+
constructor(chain: string, address: string);
|
|
18
|
+
}
|
|
19
|
+
export interface ContractRegistry {
|
|
20
|
+
contracts(chain: string, guardianSetIndex?: number): Promise<ChainContracts>;
|
|
21
|
+
assertRegistered(chain: string, address: string, guardianSetIndex?: number): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare const sameAddress: (a: string, b: string) => boolean;
|
|
24
|
+
export declare const guardianMessage: (index: number, members: string[]) => string;
|
|
25
|
+
export declare const verifyGuardianSets: (sets: GuardianSet[], genesis?: readonly string[]) => Promise<GuardianSet>;
|
|
26
|
+
export declare const verifyManifestSignatures: (manifest: string, signatures: string[], members: string[]) => Promise<void>;
|
|
27
|
+
export declare const verifyContracts: (response: ContractsResponse, genesis?: readonly string[]) => Promise<ContractManifest>;
|
|
28
|
+
export declare class ApiContractRegistry implements ContractRegistry {
|
|
29
|
+
private api;
|
|
30
|
+
private cached?;
|
|
31
|
+
private inflight?;
|
|
32
|
+
constructor(api: Client<typeof BlinkApi>);
|
|
33
|
+
contracts(chain: string, guardianSetIndex?: number): Promise<ChainContracts>;
|
|
34
|
+
assertRegistered(chain: string, address: string, guardianSetIndex?: number): Promise<void>;
|
|
35
|
+
private lookup;
|
|
36
|
+
private manifest;
|
|
37
|
+
private current;
|
|
38
|
+
private refresh;
|
|
39
|
+
private fetch;
|
|
40
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { recoverMessageAddress } from "viem";
|
|
2
|
+
export const GENESIS_GUARDIANS = [
|
|
3
|
+
"0x4FbA3CE3CC1eb4297E274f42B1C252Ba42F886a6",
|
|
4
|
+
"0x024721C2d41739BCdE70EEc65e73E2B27726dDA9",
|
|
5
|
+
"0x9E497d060D1543Bd360db003Da7a13924a2dB8cb",
|
|
6
|
+
];
|
|
7
|
+
export const CONTRACTS_REFRESH_MS = 2 * 60 * 60 * 1000;
|
|
8
|
+
export class UnregisteredAddressError extends Error {
|
|
9
|
+
chain;
|
|
10
|
+
address;
|
|
11
|
+
constructor(chain, address) {
|
|
12
|
+
super(`${address} is not a registered contract on ${chain}`);
|
|
13
|
+
this.chain = chain;
|
|
14
|
+
this.address = address;
|
|
15
|
+
this.name = "UnregisteredAddressError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const sameAddress = (a, b) => a.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
19
|
+
export const guardianMessage = (index, members) => `blink-guardians:${index}:${members.map((m) => m.toLowerCase()).join(",")}`;
|
|
20
|
+
const recoveredSigners = async (message, signatures) => {
|
|
21
|
+
const signers = new Set();
|
|
22
|
+
for (const signature of signatures) {
|
|
23
|
+
const signer = await recoverMessageAddress({
|
|
24
|
+
message,
|
|
25
|
+
signature: signature,
|
|
26
|
+
});
|
|
27
|
+
signers.add(signer.toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
return signers;
|
|
30
|
+
};
|
|
31
|
+
const sameMembers = (a, b) => a.length === b.length && a.every((m, i) => sameAddress(m, b[i]));
|
|
32
|
+
export const verifyGuardianSets = async (sets, genesis = GENESIS_GUARDIANS) => {
|
|
33
|
+
const first = sets[0];
|
|
34
|
+
if (!first || first.index !== 0 || !sameMembers(first.members, genesis)) {
|
|
35
|
+
throw new Error("guardian set 0 does not match the pinned genesis set");
|
|
36
|
+
}
|
|
37
|
+
for (let i = 1; i < sets.length; i++) {
|
|
38
|
+
const previous = sets[i - 1];
|
|
39
|
+
const set = sets[i];
|
|
40
|
+
if (set.index !== i) {
|
|
41
|
+
throw new Error(`guardian set ${set.index} is out of sequence`);
|
|
42
|
+
}
|
|
43
|
+
if (set.members.length === 0 ||
|
|
44
|
+
new Set(set.members.map((m) => m.toLowerCase())).size !==
|
|
45
|
+
set.members.length) {
|
|
46
|
+
throw new Error(`guardian set ${i} members must be non-empty and distinct`);
|
|
47
|
+
}
|
|
48
|
+
const signers = await recoveredSigners(guardianMessage(i, set.members), set.signatures);
|
|
49
|
+
const approvals = previous.members.filter((m) => signers.has(m.toLowerCase())).length;
|
|
50
|
+
const quorum = Math.floor(previous.members.length / 2) + 1;
|
|
51
|
+
if (approvals < quorum) {
|
|
52
|
+
throw new Error(`guardian set ${i} has ${approvals} of ${previous.members.length} approvals, quorum is ${quorum}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return sets[sets.length - 1];
|
|
56
|
+
};
|
|
57
|
+
export const verifyManifestSignatures = async (manifest, signatures, members) => {
|
|
58
|
+
const signers = await recoveredSigners({ raw: new TextEncoder().encode(manifest) }, signatures);
|
|
59
|
+
const missing = members.find((m) => !signers.has(m.toLowerCase()));
|
|
60
|
+
if (missing) {
|
|
61
|
+
throw new Error(`guardian ${missing} has not signed the contract manifest`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const isStringList = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
65
|
+
const parseManifest = (raw) => {
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
if (typeof parsed !== "object" ||
|
|
68
|
+
parsed === null ||
|
|
69
|
+
typeof parsed.guardian_set_index !==
|
|
70
|
+
"number" ||
|
|
71
|
+
typeof parsed.chains !== "object") {
|
|
72
|
+
throw new Error("contract manifest is malformed");
|
|
73
|
+
}
|
|
74
|
+
const { guardian_set_index, chains } = parsed;
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [chain, entry] of Object.entries(chains)) {
|
|
77
|
+
const { router, escrow, swap_programs } = entry;
|
|
78
|
+
if (typeof router !== "string" ||
|
|
79
|
+
(escrow !== undefined && typeof escrow !== "string") ||
|
|
80
|
+
(swap_programs !== undefined && !isStringList(swap_programs))) {
|
|
81
|
+
throw new Error(`contract manifest entry for ${chain} is malformed`);
|
|
82
|
+
}
|
|
83
|
+
out[chain] = { router, escrow, swapPrograms: swap_programs ?? [] };
|
|
84
|
+
}
|
|
85
|
+
return { guardianSetIndex: guardian_set_index, chains: out };
|
|
86
|
+
};
|
|
87
|
+
export const verifyContracts = async (response, genesis = GENESIS_GUARDIANS) => {
|
|
88
|
+
const latest = await verifyGuardianSets(response.guardianSets, genesis);
|
|
89
|
+
await verifyManifestSignatures(response.manifest, response.signatures, latest.members);
|
|
90
|
+
const manifest = parseManifest(response.manifest);
|
|
91
|
+
if (manifest.guardianSetIndex !== latest.index) {
|
|
92
|
+
throw new Error(`contract manifest is signed under guardian set ${manifest.guardianSetIndex}, latest is ${latest.index}`);
|
|
93
|
+
}
|
|
94
|
+
return manifest;
|
|
95
|
+
};
|
|
96
|
+
export class ApiContractRegistry {
|
|
97
|
+
api;
|
|
98
|
+
cached;
|
|
99
|
+
inflight;
|
|
100
|
+
constructor(api) {
|
|
101
|
+
this.api = api;
|
|
102
|
+
}
|
|
103
|
+
async contracts(chain, guardianSetIndex) {
|
|
104
|
+
return this.lookup(guardianSetIndex, (manifest) => manifest.chains[chain], () => new UnregisteredAddressError(chain, "every contract"));
|
|
105
|
+
}
|
|
106
|
+
async assertRegistered(chain, address, guardianSetIndex) {
|
|
107
|
+
await this.lookup(guardianSetIndex, (manifest) => {
|
|
108
|
+
const contracts = manifest.chains[chain];
|
|
109
|
+
return [contracts?.router, contracts?.escrow].find((registered) => registered !== undefined && sameAddress(registered, address));
|
|
110
|
+
}, () => new UnregisteredAddressError(chain, address));
|
|
111
|
+
}
|
|
112
|
+
async lookup(guardianSetIndex, pick, missing) {
|
|
113
|
+
const first = pick(await this.manifest(guardianSetIndex));
|
|
114
|
+
if (first !== undefined) {
|
|
115
|
+
return first;
|
|
116
|
+
}
|
|
117
|
+
await this.refresh();
|
|
118
|
+
const second = pick(await this.manifest(guardianSetIndex));
|
|
119
|
+
if (second !== undefined) {
|
|
120
|
+
return second;
|
|
121
|
+
}
|
|
122
|
+
throw missing();
|
|
123
|
+
}
|
|
124
|
+
async manifest(guardianSetIndex) {
|
|
125
|
+
let current = await this.current();
|
|
126
|
+
if (guardianSetIndex !== undefined &&
|
|
127
|
+
guardianSetIndex > current.guardianSetIndex) {
|
|
128
|
+
current = await this.refresh();
|
|
129
|
+
}
|
|
130
|
+
if (guardianSetIndex !== undefined &&
|
|
131
|
+
guardianSetIndex !== current.guardianSetIndex) {
|
|
132
|
+
throw new Error(`quote was issued under guardian set ${guardianSetIndex}, latest is ${current.guardianSetIndex}`);
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
current() {
|
|
137
|
+
if (!this.cached ||
|
|
138
|
+
Date.now() - this.cached.fetchedAt > CONTRACTS_REFRESH_MS) {
|
|
139
|
+
return this.refresh();
|
|
140
|
+
}
|
|
141
|
+
return Promise.resolve(this.cached.manifest);
|
|
142
|
+
}
|
|
143
|
+
refresh() {
|
|
144
|
+
if (!this.inflight) {
|
|
145
|
+
this.inflight = this.fetch().finally(() => {
|
|
146
|
+
this.inflight = undefined;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return this.inflight;
|
|
150
|
+
}
|
|
151
|
+
async fetch() {
|
|
152
|
+
const manifest = await verifyContracts(await this.api.getContracts({}));
|
|
153
|
+
if (this.cached &&
|
|
154
|
+
manifest.guardianSetIndex < this.cached.manifest.guardianSetIndex) {
|
|
155
|
+
throw new Error(`contract manifest rolled back to guardian set ${manifest.guardianSetIndex}`);
|
|
156
|
+
}
|
|
157
|
+
this.cached = { manifest, fetchedAt: Date.now() };
|
|
158
|
+
return manifest;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/dist/deposit.d.ts
CHANGED
|
@@ -2,17 +2,19 @@ import type { BlinkClient } from "./client.js";
|
|
|
2
2
|
import type { ChainStatus, OrderParams } from "./gen/blink/v1/api_pb.js";
|
|
3
3
|
import { type ConnectedWallet, type WalletKind } from "./wallets.js";
|
|
4
4
|
export interface DepositStep {
|
|
5
|
-
kind: "approve" | "deposit";
|
|
5
|
+
kind: "approve" | "deposit" | "swap";
|
|
6
6
|
fee?: bigint;
|
|
7
7
|
run: () => Promise<string>;
|
|
8
8
|
}
|
|
9
9
|
export interface GaslessQuote {
|
|
10
|
+
guardianSetIndex?: number;
|
|
10
11
|
sponsorAddress?: string;
|
|
11
12
|
sponsorGasBudget?: bigint;
|
|
12
13
|
depositDeadline?: string;
|
|
13
14
|
}
|
|
14
15
|
export type SvmComputeUnitPrice = "auto" | bigint;
|
|
15
16
|
export interface DepositOptions {
|
|
17
|
+
guardianSetIndex?: number;
|
|
16
18
|
directTransfer?: boolean;
|
|
17
19
|
depositDeadline?: string;
|
|
18
20
|
svmComputeUnitPrice?: SvmComputeUnitPrice;
|
|
@@ -43,20 +45,20 @@ export declare class EvmChainAdapter implements ChainAdapter {
|
|
|
43
45
|
gaslessSteps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, quote: GaslessQuote): Promise<DepositStep[]>;
|
|
44
46
|
}
|
|
45
47
|
export declare class SvmChainAdapter implements ChainAdapter {
|
|
48
|
+
private client;
|
|
46
49
|
private chain;
|
|
47
|
-
private client?;
|
|
48
50
|
readonly kind = "svm";
|
|
49
|
-
constructor(
|
|
51
|
+
constructor(client: BlinkClient, chain: ChainStatus);
|
|
50
52
|
balance(owner: string, token: string): Promise<bigint>;
|
|
51
53
|
canPayGas(owner: string): Promise<boolean>;
|
|
52
54
|
steps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, options?: DepositOptions): Promise<DepositStep[]>;
|
|
53
55
|
gaslessSteps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, quote: GaslessQuote): Promise<DepositStep[]>;
|
|
54
56
|
}
|
|
55
57
|
export declare class SuiChainAdapter implements ChainAdapter {
|
|
58
|
+
private client;
|
|
56
59
|
private chain;
|
|
57
|
-
private client?;
|
|
58
60
|
readonly kind = "sui";
|
|
59
|
-
constructor(
|
|
61
|
+
constructor(client: BlinkClient, chain: ChainStatus);
|
|
60
62
|
balance(owner: string, token: string): Promise<bigint>;
|
|
61
63
|
canPayGas(owner: string): Promise<boolean>;
|
|
62
64
|
steps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, options?: DepositOptions): Promise<DepositStep[]>;
|
package/dist/deposit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
1
2
|
import { formatAmount } from "./amount.js";
|
|
2
3
|
import { buildEvmDepositCall, detectPermit, encodeApprove, evmAllowance, evmAllowanceSlot, evmEstimateGas, evmGasPrice, permit2WitnessDepositTypedData, permit2WitnessSwapDepositTypedData, evmBalance, isNativeEvmToken, PERMIT2_ADDRESS, permit2TypedData, permitDeadline, permitTypedData, randomPermit2Nonce, splitSignature, usesDirectTransfer, waitForEvmTransaction, } from "./evm.js";
|
|
3
4
|
import { assertQuoteHash } from "./hash.js";
|
|
4
5
|
import { assertDepositDeadline, isSourceSwap } from "./order.js";
|
|
5
|
-
import { SUI_COIN_TYPE, buildSuiDeposit,
|
|
6
|
+
import { SUI_COIN_TYPE, buildSuiDeposit, buildSuiSourceSwapOpen, sponsorSuiTransaction, suiBalance, } from "./sui.js";
|
|
6
7
|
import { buildSvmDeposit, buildSvmSwapDeposit, svmBalance } from "./svm.js";
|
|
7
8
|
import { sendEvmCalls, sendEvmTransaction, signAndSendSuiTransaction, signAndSendSvmTransaction, signSuiTransaction, signSvmTransaction, signEvmTypedData, supportsAtomicBatch, switchEvmChain, } from "./wallets.js";
|
|
8
9
|
const trackGaslessSubmission = async (client, quoteHash) => {
|
|
@@ -17,6 +18,43 @@ const trackGaslessSubmission = async (client, quoteHash) => {
|
|
|
17
18
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
18
19
|
}
|
|
19
20
|
};
|
|
21
|
+
const SOURCE_SWAP_POLL_MS = 2000;
|
|
22
|
+
const SOURCE_SWAP_SETTLE_MS = 120_000;
|
|
23
|
+
const SOURCE_SWAP_UNSEEN_MS = 420_000;
|
|
24
|
+
const unlessNotFound = async (call) => {
|
|
25
|
+
try {
|
|
26
|
+
return await call();
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (e instanceof ConnectError && e.code === Code.NotFound) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const trackSourceSwap = async (client, quoteHash) => {
|
|
36
|
+
const started = Date.now();
|
|
37
|
+
for (;;) {
|
|
38
|
+
const swap = await unlessNotFound(() => client.blink.getSourceSwap({ quoteHash }));
|
|
39
|
+
if (swap?.status === "executed") {
|
|
40
|
+
return swap.swapTx ?? swap.openTx;
|
|
41
|
+
}
|
|
42
|
+
const overdue = swap
|
|
43
|
+
? Date.parse(swap.expiresAt) + SOURCE_SWAP_SETTLE_MS < Date.now()
|
|
44
|
+
: Date.now() - started > SOURCE_SWAP_UNSEEN_MS;
|
|
45
|
+
if (swap?.status === "lapsed" || overdue) {
|
|
46
|
+
const detail = await unlessNotFound(() => client.blink.getOrder({ quoteHash }));
|
|
47
|
+
if (detail?.order?.srcTx) {
|
|
48
|
+
return detail.order.srcTx;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`the source swap did not run (${swap?.reason ?? "expired"}); your funds never left your wallet`);
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, SOURCE_SWAP_POLL_MS));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const sourceSwapStep = (client, quoteHash, params) => isSourceSwap(params)
|
|
56
|
+
? [{ kind: "swap", run: () => trackSourceSwap(client, quoteHash) }]
|
|
57
|
+
: [];
|
|
20
58
|
const rpcUrlOf = (chain) => {
|
|
21
59
|
if (!chain.rpcUrl) {
|
|
22
60
|
throw new Error(`no public rpc configured for ${chain.chain}`);
|
|
@@ -27,9 +65,6 @@ const svmComputeUnitPriceMicroLamports = async (client, chain, price) => {
|
|
|
27
65
|
if (price !== "auto") {
|
|
28
66
|
return price;
|
|
29
67
|
}
|
|
30
|
-
if (!client) {
|
|
31
|
-
throw new Error("auto svm priority fees need an api client");
|
|
32
|
-
}
|
|
33
68
|
const fee = await client.blink.getPriorityFee({ chain });
|
|
34
69
|
return fee.computeUnitPriceMicroLamports;
|
|
35
70
|
};
|
|
@@ -102,6 +137,9 @@ export class EvmChainAdapter {
|
|
|
102
137
|
...call,
|
|
103
138
|
});
|
|
104
139
|
};
|
|
140
|
+
const registry = this.client.contracts;
|
|
141
|
+
const guardianSetIndex = options?.guardianSetIndex;
|
|
142
|
+
await registry.assertRegistered(params.srcChain, params.routerAddress, guardianSetIndex);
|
|
105
143
|
const base = {
|
|
106
144
|
quoteHash,
|
|
107
145
|
params,
|
|
@@ -113,16 +151,17 @@ export class EvmChainAdapter {
|
|
|
113
151
|
await this.assertCanPay(wallet.address, fee);
|
|
114
152
|
return [{ kind: "deposit", fee, run: () => send(call) }];
|
|
115
153
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
154
|
+
const swap = isSourceSwap(params)
|
|
155
|
+
? await this.client.blink.getSwapInstructions({ quoteHash })
|
|
156
|
+
: undefined;
|
|
157
|
+
if (swap) {
|
|
158
|
+
await registry.assertRegistered(params.srcChain, swap.routerAddress, guardianSetIndex);
|
|
159
|
+
}
|
|
160
|
+
const { router } = await registry.contracts(params.srcChain, guardianSetIndex);
|
|
161
|
+
const request = { ...base, swap, router };
|
|
123
162
|
const call = buildEvmDepositCall(request);
|
|
124
163
|
const target = call.to;
|
|
125
|
-
const permitTarget =
|
|
164
|
+
const permitTarget = swap ? target : router;
|
|
126
165
|
const deposit = { kind: "deposit", run: () => send(call) };
|
|
127
166
|
if (native) {
|
|
128
167
|
deposit.fee = await feeOf(call);
|
|
@@ -222,10 +261,7 @@ export class EvmChainAdapter {
|
|
|
222
261
|
}
|
|
223
262
|
assertQuoteHash(quoteHash, params);
|
|
224
263
|
assertDepositDeadline(quote.depositDeadline);
|
|
225
|
-
const spender = this.
|
|
226
|
-
if (!spender) {
|
|
227
|
-
throw new Error(`no router configured for ${this.chain.chain}`);
|
|
228
|
-
}
|
|
264
|
+
const { router: spender } = await this.client.contracts.contracts(params.srcChain, quote.guardianSetIndex);
|
|
229
265
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
230
266
|
const amountIn = BigInt(params.amountIn);
|
|
231
267
|
if (isNativeEvmToken(params.srcToken)) {
|
|
@@ -292,12 +328,12 @@ export class EvmChainAdapter {
|
|
|
292
328
|
}
|
|
293
329
|
}
|
|
294
330
|
export class SvmChainAdapter {
|
|
295
|
-
chain;
|
|
296
331
|
client;
|
|
332
|
+
chain;
|
|
297
333
|
kind = "svm";
|
|
298
|
-
constructor(
|
|
299
|
-
this.chain = chain;
|
|
334
|
+
constructor(client, chain) {
|
|
300
335
|
this.client = client;
|
|
336
|
+
this.chain = chain;
|
|
301
337
|
}
|
|
302
338
|
balance(owner, token) {
|
|
303
339
|
return svmBalance(rpcUrlOf(this.chain), token, owner);
|
|
@@ -312,16 +348,12 @@ export class SvmChainAdapter {
|
|
|
312
348
|
}
|
|
313
349
|
assertQuoteHash(quoteHash, params);
|
|
314
350
|
assertDepositDeadline(options?.depositDeadline);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
351
|
+
const client = this.client;
|
|
352
|
+
const registry = client.contracts;
|
|
353
|
+
const guardianSetIndex = options?.guardianSetIndex;
|
|
318
354
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
319
355
|
const computeUnitPrice = options?.svmComputeUnitPrice ?? "auto";
|
|
320
356
|
if (isSourceSwap(params)) {
|
|
321
|
-
const client = this.client;
|
|
322
|
-
if (!client) {
|
|
323
|
-
throw new Error("svm swap deposits need an api client");
|
|
324
|
-
}
|
|
325
357
|
return [
|
|
326
358
|
{
|
|
327
359
|
kind: "deposit",
|
|
@@ -332,9 +364,10 @@ export class SvmChainAdapter {
|
|
|
332
364
|
if (instructions.payload.case !== "svm") {
|
|
333
365
|
throw new Error("expected svm swap instructions");
|
|
334
366
|
}
|
|
367
|
+
await registry.assertRegistered(params.srcChain, instructions.routerAddress, guardianSetIndex);
|
|
368
|
+
const { swapPrograms } = await registry.contracts(params.srcChain, guardianSetIndex);
|
|
335
369
|
const transaction = await buildSvmSwapDeposit({
|
|
336
370
|
rpcUrl,
|
|
337
|
-
chain: this.chain.chain,
|
|
338
371
|
programId: instructions.routerAddress,
|
|
339
372
|
owner: wallet.address,
|
|
340
373
|
srcToken: params.srcToken,
|
|
@@ -344,7 +377,7 @@ export class SvmChainAdapter {
|
|
|
344
377
|
quoteHash,
|
|
345
378
|
swap: instructions.payload.value,
|
|
346
379
|
computeUnitPriceMicroLamports: await svmComputeUnitPriceMicroLamports(client, this.chain.chain, computeUnitPrice),
|
|
347
|
-
|
|
380
|
+
swapPrograms,
|
|
348
381
|
});
|
|
349
382
|
return signAndSendSvmTransaction(wallet.wallet, wallet.address, transaction);
|
|
350
383
|
},
|
|
@@ -355,6 +388,7 @@ export class SvmChainAdapter {
|
|
|
355
388
|
{
|
|
356
389
|
kind: "deposit",
|
|
357
390
|
run: async () => {
|
|
391
|
+
await registry.assertRegistered(params.srcChain, params.routerAddress, guardianSetIndex);
|
|
358
392
|
const transaction = await buildSvmDeposit({
|
|
359
393
|
rpcUrl,
|
|
360
394
|
programId: params.routerAddress,
|
|
@@ -362,7 +396,7 @@ export class SvmChainAdapter {
|
|
|
362
396
|
token: params.srcToken,
|
|
363
397
|
amount: BigInt(params.amountIn),
|
|
364
398
|
quoteHash,
|
|
365
|
-
computeUnitPriceMicroLamports: await svmComputeUnitPriceMicroLamports(
|
|
399
|
+
computeUnitPriceMicroLamports: await svmComputeUnitPriceMicroLamports(client, this.chain.chain, computeUnitPrice),
|
|
366
400
|
});
|
|
367
401
|
return signAndSendSvmTransaction(wallet.wallet, wallet.address, transaction);
|
|
368
402
|
},
|
|
@@ -385,15 +419,13 @@ export class SvmChainAdapter {
|
|
|
385
419
|
if (!sponsor) {
|
|
386
420
|
throw new Error("gasless deposits are unavailable on svm");
|
|
387
421
|
}
|
|
388
|
-
if (!this.client) {
|
|
389
|
-
throw new Error("gasless deposits need an api client");
|
|
390
|
-
}
|
|
391
422
|
const client = this.client;
|
|
392
423
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
393
424
|
return [
|
|
394
425
|
{
|
|
395
426
|
kind: "deposit",
|
|
396
427
|
run: async () => {
|
|
428
|
+
await client.contracts.assertRegistered(params.srcChain, params.routerAddress, quote.guardianSetIndex);
|
|
397
429
|
const transaction = await buildSvmDeposit({
|
|
398
430
|
rpcUrl,
|
|
399
431
|
programId: params.routerAddress,
|
|
@@ -419,12 +451,12 @@ export class SvmChainAdapter {
|
|
|
419
451
|
}
|
|
420
452
|
}
|
|
421
453
|
export class SuiChainAdapter {
|
|
422
|
-
chain;
|
|
423
454
|
client;
|
|
455
|
+
chain;
|
|
424
456
|
kind = "sui";
|
|
425
|
-
constructor(
|
|
426
|
-
this.chain = chain;
|
|
457
|
+
constructor(client, chain) {
|
|
427
458
|
this.client = client;
|
|
459
|
+
this.chain = chain;
|
|
428
460
|
}
|
|
429
461
|
balance(owner, token) {
|
|
430
462
|
return suiBalance(rpcUrlOf(this.chain), token, owner);
|
|
@@ -439,20 +471,17 @@ export class SuiChainAdapter {
|
|
|
439
471
|
}
|
|
440
472
|
assertQuoteHash(quoteHash, params);
|
|
441
473
|
assertDepositDeadline(options?.depositDeadline);
|
|
442
|
-
const escrow = this.chain.escrow;
|
|
443
|
-
if (!escrow) {
|
|
444
|
-
throw new Error("sui deposits are unavailable");
|
|
445
|
-
}
|
|
446
|
-
if (isSourceSwap(params) && !this.client) {
|
|
447
|
-
throw new Error("sui swap deposits need an api client");
|
|
448
|
-
}
|
|
449
474
|
const client = this.client;
|
|
450
|
-
const
|
|
475
|
+
const chain = this.chain;
|
|
451
476
|
return [
|
|
452
477
|
{
|
|
453
478
|
kind: "deposit",
|
|
454
|
-
run: async () => signAndSendSuiTransaction(wallet.wallet, wallet.address, await suiDepositTransaction(client,
|
|
479
|
+
run: async () => signAndSendSuiTransaction(wallet.wallet, wallet.address, await suiDepositTransaction(client, chain, wallet.address, quoteHash, params, {
|
|
480
|
+
guardianSetIndex: options?.guardianSetIndex,
|
|
481
|
+
gasless: false,
|
|
482
|
+
})),
|
|
455
483
|
},
|
|
484
|
+
...sourceSwapStep(client, quoteHash, params),
|
|
456
485
|
];
|
|
457
486
|
}
|
|
458
487
|
async gaslessSteps(wallet, quoteHash, params, quote) {
|
|
@@ -461,25 +490,22 @@ export class SuiChainAdapter {
|
|
|
461
490
|
}
|
|
462
491
|
assertQuoteHash(quoteHash, params);
|
|
463
492
|
assertDepositDeadline(quote.depositDeadline);
|
|
464
|
-
const escrow = this.chain.escrow;
|
|
465
|
-
if (!escrow) {
|
|
466
|
-
throw new Error("sui deposits are unavailable");
|
|
467
|
-
}
|
|
468
493
|
const sponsor = quote.sponsorAddress;
|
|
469
494
|
const gasBudget = quote.sponsorGasBudget;
|
|
470
495
|
if (!sponsor || gasBudget === undefined) {
|
|
471
496
|
throw new Error("gasless deposits are unavailable on sui");
|
|
472
497
|
}
|
|
473
|
-
if (!this.client) {
|
|
474
|
-
throw new Error("gasless deposits need an api client");
|
|
475
|
-
}
|
|
476
498
|
const client = this.client;
|
|
477
|
-
const
|
|
499
|
+
const chain = this.chain;
|
|
500
|
+
const rpcUrl = rpcUrlOf(chain);
|
|
478
501
|
return [
|
|
479
502
|
{
|
|
480
503
|
kind: "deposit",
|
|
481
504
|
run: async () => {
|
|
482
|
-
const transaction = await sponsorSuiTransaction(rpcUrl, await suiDepositTransaction(client,
|
|
505
|
+
const transaction = await sponsorSuiTransaction(rpcUrl, await suiDepositTransaction(client, chain, wallet.address, quoteHash, params, {
|
|
506
|
+
guardianSetIndex: quote.guardianSetIndex,
|
|
507
|
+
gasless: true,
|
|
508
|
+
}), sponsor, gasBudget);
|
|
483
509
|
const signed = await signSuiTransaction(wallet.wallet, wallet.address, transaction);
|
|
484
510
|
await client.blink.submitGaslessOrder({
|
|
485
511
|
quoteHash,
|
|
@@ -491,10 +517,18 @@ export class SuiChainAdapter {
|
|
|
491
517
|
return trackGaslessSubmission(client, quoteHash);
|
|
492
518
|
},
|
|
493
519
|
},
|
|
520
|
+
...sourceSwapStep(client, quoteHash, params),
|
|
494
521
|
];
|
|
495
522
|
}
|
|
496
523
|
}
|
|
497
|
-
const suiDepositTransaction = async (client,
|
|
524
|
+
const suiDepositTransaction = async (client, chain, owner, quoteHash, params, { guardianSetIndex, gasless }) => {
|
|
525
|
+
const rpcUrl = rpcUrlOf(chain);
|
|
526
|
+
const registry = client.contracts;
|
|
527
|
+
await registry.assertRegistered(params.srcChain, params.routerAddress, guardianSetIndex);
|
|
528
|
+
const { escrow } = await registry.contracts(params.srcChain, guardianSetIndex);
|
|
529
|
+
if (!escrow) {
|
|
530
|
+
throw new Error("sui deposits are unavailable");
|
|
531
|
+
}
|
|
498
532
|
if (!isSourceSwap(params)) {
|
|
499
533
|
return buildSuiDeposit({
|
|
500
534
|
rpcUrl,
|
|
@@ -506,32 +540,29 @@ const suiDepositTransaction = async (client, rpcUrl, escrow, owner, quoteHash, p
|
|
|
506
540
|
quoteHash,
|
|
507
541
|
});
|
|
508
542
|
}
|
|
509
|
-
if (!
|
|
510
|
-
throw new Error("sui
|
|
511
|
-
}
|
|
512
|
-
const instructions = await client.blink.getSwapInstructions({ quoteHash });
|
|
513
|
-
if (instructions.payload.case !== "sui") {
|
|
514
|
-
throw new Error("expected sui swap instructions");
|
|
543
|
+
if (!chain.swapExecutor) {
|
|
544
|
+
throw new Error("sui source swaps are unavailable");
|
|
515
545
|
}
|
|
516
|
-
return
|
|
546
|
+
return buildSuiSourceSwapOpen({
|
|
517
547
|
rpcUrl,
|
|
518
548
|
packageId: params.routerAddress,
|
|
519
549
|
escrowObject: escrow,
|
|
550
|
+
swapExecutor: chain.swapExecutor,
|
|
520
551
|
owner,
|
|
521
552
|
srcToken: params.srcToken,
|
|
522
553
|
bridgeToken: params.bridgeTokenSrc,
|
|
523
554
|
amountIn: BigInt(params.amountIn),
|
|
524
555
|
minBridgeOut: BigInt(params.minBridgeOut),
|
|
525
556
|
quoteHash,
|
|
526
|
-
|
|
557
|
+
gasless,
|
|
527
558
|
});
|
|
528
559
|
};
|
|
529
560
|
export const createChainAdapter = (client, chain, chainId) => {
|
|
530
561
|
if (chain.vm === "svm") {
|
|
531
|
-
return new SvmChainAdapter(
|
|
562
|
+
return new SvmChainAdapter(client, chain);
|
|
532
563
|
}
|
|
533
564
|
if (chain.vm === "sui") {
|
|
534
|
-
return new SuiChainAdapter(
|
|
565
|
+
return new SuiChainAdapter(client, chain);
|
|
535
566
|
}
|
|
536
567
|
if (chainId === undefined) {
|
|
537
568
|
throw new Error(`unknown chain id for ${chain.chain}`);
|