@mayanfinance/blink-sdk 0.1.8 → 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 +9 -5
- package/dist/deposit.js +106 -60
- 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 +155 -8
- package/dist/gen/blink/v1/api_pb.js +40 -10
- 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 +36 -26
- package/dist/sui.d.ts +10 -21
- package/dist/sui.js +55 -168
- package/dist/svm.d.ts +4 -4
- package/dist/svm.js +209 -211
- 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,18 +2,22 @@ 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
|
}
|
|
15
|
+
export type SvmComputeUnitPrice = "auto" | bigint;
|
|
14
16
|
export interface DepositOptions {
|
|
17
|
+
guardianSetIndex?: number;
|
|
15
18
|
directTransfer?: boolean;
|
|
16
19
|
depositDeadline?: string;
|
|
20
|
+
svmComputeUnitPrice?: SvmComputeUnitPrice;
|
|
17
21
|
}
|
|
18
22
|
export interface ChainAdapter {
|
|
19
23
|
readonly kind: WalletKind;
|
|
@@ -41,20 +45,20 @@ export declare class EvmChainAdapter implements ChainAdapter {
|
|
|
41
45
|
gaslessSteps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, quote: GaslessQuote): Promise<DepositStep[]>;
|
|
42
46
|
}
|
|
43
47
|
export declare class SvmChainAdapter implements ChainAdapter {
|
|
48
|
+
private client;
|
|
44
49
|
private chain;
|
|
45
|
-
private client?;
|
|
46
50
|
readonly kind = "svm";
|
|
47
|
-
constructor(
|
|
51
|
+
constructor(client: BlinkClient, chain: ChainStatus);
|
|
48
52
|
balance(owner: string, token: string): Promise<bigint>;
|
|
49
53
|
canPayGas(owner: string): Promise<boolean>;
|
|
50
54
|
steps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, options?: DepositOptions): Promise<DepositStep[]>;
|
|
51
55
|
gaslessSteps(wallet: ConnectedWallet, quoteHash: string, params: OrderParams, quote: GaslessQuote): Promise<DepositStep[]>;
|
|
52
56
|
}
|
|
53
57
|
export declare class SuiChainAdapter implements ChainAdapter {
|
|
58
|
+
private client;
|
|
54
59
|
private chain;
|
|
55
|
-
private client?;
|
|
56
60
|
readonly kind = "sui";
|
|
57
|
-
constructor(
|
|
61
|
+
constructor(client: BlinkClient, chain: ChainStatus);
|
|
58
62
|
balance(owner: string, token: string): Promise<bigint>;
|
|
59
63
|
canPayGas(owner: string): Promise<boolean>;
|
|
60
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,14 +18,59 @@ 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}`);
|
|
23
61
|
}
|
|
24
62
|
return chain.rpcUrl;
|
|
25
63
|
};
|
|
64
|
+
const svmComputeUnitPriceMicroLamports = async (client, chain, price) => {
|
|
65
|
+
if (price !== "auto") {
|
|
66
|
+
return price;
|
|
67
|
+
}
|
|
68
|
+
const fee = await client.blink.getPriorityFee({ chain });
|
|
69
|
+
return fee.computeUnitPriceMicroLamports;
|
|
70
|
+
};
|
|
26
71
|
const NATIVE_EVM_TOKEN = "0x0000000000000000000000000000000000000000";
|
|
27
72
|
const SVM_MIN_FEE_LAMPORTS = 2000000n;
|
|
73
|
+
const SVM_GASLESS_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 10000n;
|
|
28
74
|
const SUI_MIN_FEE_MIST = 20000000n;
|
|
29
75
|
export class InsufficientGasError extends Error {
|
|
30
76
|
required;
|
|
@@ -91,6 +137,9 @@ export class EvmChainAdapter {
|
|
|
91
137
|
...call,
|
|
92
138
|
});
|
|
93
139
|
};
|
|
140
|
+
const registry = this.client.contracts;
|
|
141
|
+
const guardianSetIndex = options?.guardianSetIndex;
|
|
142
|
+
await registry.assertRegistered(params.srcChain, params.routerAddress, guardianSetIndex);
|
|
94
143
|
const base = {
|
|
95
144
|
quoteHash,
|
|
96
145
|
params,
|
|
@@ -102,16 +151,17 @@ export class EvmChainAdapter {
|
|
|
102
151
|
await this.assertCanPay(wallet.address, fee);
|
|
103
152
|
return [{ kind: "deposit", fee, run: () => send(call) }];
|
|
104
153
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
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 };
|
|
112
162
|
const call = buildEvmDepositCall(request);
|
|
113
163
|
const target = call.to;
|
|
114
|
-
const permitTarget =
|
|
164
|
+
const permitTarget = swap ? target : router;
|
|
115
165
|
const deposit = { kind: "deposit", run: () => send(call) };
|
|
116
166
|
if (native) {
|
|
117
167
|
deposit.fee = await feeOf(call);
|
|
@@ -211,10 +261,7 @@ export class EvmChainAdapter {
|
|
|
211
261
|
}
|
|
212
262
|
assertQuoteHash(quoteHash, params);
|
|
213
263
|
assertDepositDeadline(quote.depositDeadline);
|
|
214
|
-
const spender = this.
|
|
215
|
-
if (!spender) {
|
|
216
|
-
throw new Error(`no router configured for ${this.chain.chain}`);
|
|
217
|
-
}
|
|
264
|
+
const { router: spender } = await this.client.contracts.contracts(params.srcChain, quote.guardianSetIndex);
|
|
218
265
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
219
266
|
const amountIn = BigInt(params.amountIn);
|
|
220
267
|
if (isNativeEvmToken(params.srcToken)) {
|
|
@@ -281,12 +328,12 @@ export class EvmChainAdapter {
|
|
|
281
328
|
}
|
|
282
329
|
}
|
|
283
330
|
export class SvmChainAdapter {
|
|
284
|
-
chain;
|
|
285
331
|
client;
|
|
332
|
+
chain;
|
|
286
333
|
kind = "svm";
|
|
287
|
-
constructor(
|
|
288
|
-
this.chain = chain;
|
|
334
|
+
constructor(client, chain) {
|
|
289
335
|
this.client = client;
|
|
336
|
+
this.chain = chain;
|
|
290
337
|
}
|
|
291
338
|
balance(owner, token) {
|
|
292
339
|
return svmBalance(rpcUrlOf(this.chain), token, owner);
|
|
@@ -301,15 +348,12 @@ export class SvmChainAdapter {
|
|
|
301
348
|
}
|
|
302
349
|
assertQuoteHash(quoteHash, params);
|
|
303
350
|
assertDepositDeadline(options?.depositDeadline);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
351
|
+
const client = this.client;
|
|
352
|
+
const registry = client.contracts;
|
|
353
|
+
const guardianSetIndex = options?.guardianSetIndex;
|
|
307
354
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
355
|
+
const computeUnitPrice = options?.svmComputeUnitPrice ?? "auto";
|
|
308
356
|
if (isSourceSwap(params)) {
|
|
309
|
-
const client = this.client;
|
|
310
|
-
if (!client) {
|
|
311
|
-
throw new Error("svm swap deposits need an api client");
|
|
312
|
-
}
|
|
313
357
|
return [
|
|
314
358
|
{
|
|
315
359
|
kind: "deposit",
|
|
@@ -320,9 +364,10 @@ export class SvmChainAdapter {
|
|
|
320
364
|
if (instructions.payload.case !== "svm") {
|
|
321
365
|
throw new Error("expected svm swap instructions");
|
|
322
366
|
}
|
|
367
|
+
await registry.assertRegistered(params.srcChain, instructions.routerAddress, guardianSetIndex);
|
|
368
|
+
const { swapPrograms } = await registry.contracts(params.srcChain, guardianSetIndex);
|
|
323
369
|
const transaction = await buildSvmSwapDeposit({
|
|
324
370
|
rpcUrl,
|
|
325
|
-
chain: this.chain.chain,
|
|
326
371
|
programId: instructions.routerAddress,
|
|
327
372
|
owner: wallet.address,
|
|
328
373
|
srcToken: params.srcToken,
|
|
@@ -331,7 +376,8 @@ export class SvmChainAdapter {
|
|
|
331
376
|
minBridgeOut: BigInt(params.minBridgeOut),
|
|
332
377
|
quoteHash,
|
|
333
378
|
swap: instructions.payload.value,
|
|
334
|
-
|
|
379
|
+
computeUnitPriceMicroLamports: await svmComputeUnitPriceMicroLamports(client, this.chain.chain, computeUnitPrice),
|
|
380
|
+
swapPrograms,
|
|
335
381
|
});
|
|
336
382
|
return signAndSendSvmTransaction(wallet.wallet, wallet.address, transaction);
|
|
337
383
|
},
|
|
@@ -342,6 +388,7 @@ export class SvmChainAdapter {
|
|
|
342
388
|
{
|
|
343
389
|
kind: "deposit",
|
|
344
390
|
run: async () => {
|
|
391
|
+
await registry.assertRegistered(params.srcChain, params.routerAddress, guardianSetIndex);
|
|
345
392
|
const transaction = await buildSvmDeposit({
|
|
346
393
|
rpcUrl,
|
|
347
394
|
programId: params.routerAddress,
|
|
@@ -349,6 +396,7 @@ export class SvmChainAdapter {
|
|
|
349
396
|
token: params.srcToken,
|
|
350
397
|
amount: BigInt(params.amountIn),
|
|
351
398
|
quoteHash,
|
|
399
|
+
computeUnitPriceMicroLamports: await svmComputeUnitPriceMicroLamports(client, this.chain.chain, computeUnitPrice),
|
|
352
400
|
});
|
|
353
401
|
return signAndSendSvmTransaction(wallet.wallet, wallet.address, transaction);
|
|
354
402
|
},
|
|
@@ -371,15 +419,13 @@ export class SvmChainAdapter {
|
|
|
371
419
|
if (!sponsor) {
|
|
372
420
|
throw new Error("gasless deposits are unavailable on svm");
|
|
373
421
|
}
|
|
374
|
-
if (!this.client) {
|
|
375
|
-
throw new Error("gasless deposits need an api client");
|
|
376
|
-
}
|
|
377
422
|
const client = this.client;
|
|
378
423
|
const rpcUrl = rpcUrlOf(this.chain);
|
|
379
424
|
return [
|
|
380
425
|
{
|
|
381
426
|
kind: "deposit",
|
|
382
427
|
run: async () => {
|
|
428
|
+
await client.contracts.assertRegistered(params.srcChain, params.routerAddress, quote.guardianSetIndex);
|
|
383
429
|
const transaction = await buildSvmDeposit({
|
|
384
430
|
rpcUrl,
|
|
385
431
|
programId: params.routerAddress,
|
|
@@ -387,6 +433,7 @@ export class SvmChainAdapter {
|
|
|
387
433
|
token: params.srcToken,
|
|
388
434
|
amount: BigInt(params.amountIn),
|
|
389
435
|
quoteHash,
|
|
436
|
+
computeUnitPriceMicroLamports: SVM_GASLESS_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
390
437
|
feePayer: sponsor,
|
|
391
438
|
});
|
|
392
439
|
const signed = await signSvmTransaction(wallet.wallet, wallet.address, transaction);
|
|
@@ -404,12 +451,12 @@ export class SvmChainAdapter {
|
|
|
404
451
|
}
|
|
405
452
|
}
|
|
406
453
|
export class SuiChainAdapter {
|
|
407
|
-
chain;
|
|
408
454
|
client;
|
|
455
|
+
chain;
|
|
409
456
|
kind = "sui";
|
|
410
|
-
constructor(
|
|
411
|
-
this.chain = chain;
|
|
457
|
+
constructor(client, chain) {
|
|
412
458
|
this.client = client;
|
|
459
|
+
this.chain = chain;
|
|
413
460
|
}
|
|
414
461
|
balance(owner, token) {
|
|
415
462
|
return suiBalance(rpcUrlOf(this.chain), token, owner);
|
|
@@ -424,20 +471,17 @@ export class SuiChainAdapter {
|
|
|
424
471
|
}
|
|
425
472
|
assertQuoteHash(quoteHash, params);
|
|
426
473
|
assertDepositDeadline(options?.depositDeadline);
|
|
427
|
-
const escrow = this.chain.escrow;
|
|
428
|
-
if (!escrow) {
|
|
429
|
-
throw new Error("sui deposits are unavailable");
|
|
430
|
-
}
|
|
431
|
-
if (isSourceSwap(params) && !this.client) {
|
|
432
|
-
throw new Error("sui swap deposits need an api client");
|
|
433
|
-
}
|
|
434
474
|
const client = this.client;
|
|
435
|
-
const
|
|
475
|
+
const chain = this.chain;
|
|
436
476
|
return [
|
|
437
477
|
{
|
|
438
478
|
kind: "deposit",
|
|
439
|
-
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
|
+
})),
|
|
440
483
|
},
|
|
484
|
+
...sourceSwapStep(client, quoteHash, params),
|
|
441
485
|
];
|
|
442
486
|
}
|
|
443
487
|
async gaslessSteps(wallet, quoteHash, params, quote) {
|
|
@@ -446,25 +490,22 @@ export class SuiChainAdapter {
|
|
|
446
490
|
}
|
|
447
491
|
assertQuoteHash(quoteHash, params);
|
|
448
492
|
assertDepositDeadline(quote.depositDeadline);
|
|
449
|
-
const escrow = this.chain.escrow;
|
|
450
|
-
if (!escrow) {
|
|
451
|
-
throw new Error("sui deposits are unavailable");
|
|
452
|
-
}
|
|
453
493
|
const sponsor = quote.sponsorAddress;
|
|
454
494
|
const gasBudget = quote.sponsorGasBudget;
|
|
455
495
|
if (!sponsor || gasBudget === undefined) {
|
|
456
496
|
throw new Error("gasless deposits are unavailable on sui");
|
|
457
497
|
}
|
|
458
|
-
if (!this.client) {
|
|
459
|
-
throw new Error("gasless deposits need an api client");
|
|
460
|
-
}
|
|
461
498
|
const client = this.client;
|
|
462
|
-
const
|
|
499
|
+
const chain = this.chain;
|
|
500
|
+
const rpcUrl = rpcUrlOf(chain);
|
|
463
501
|
return [
|
|
464
502
|
{
|
|
465
503
|
kind: "deposit",
|
|
466
504
|
run: async () => {
|
|
467
|
-
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);
|
|
468
509
|
const signed = await signSuiTransaction(wallet.wallet, wallet.address, transaction);
|
|
469
510
|
await client.blink.submitGaslessOrder({
|
|
470
511
|
quoteHash,
|
|
@@ -476,10 +517,18 @@ export class SuiChainAdapter {
|
|
|
476
517
|
return trackGaslessSubmission(client, quoteHash);
|
|
477
518
|
},
|
|
478
519
|
},
|
|
520
|
+
...sourceSwapStep(client, quoteHash, params),
|
|
479
521
|
];
|
|
480
522
|
}
|
|
481
523
|
}
|
|
482
|
-
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
|
+
}
|
|
483
532
|
if (!isSourceSwap(params)) {
|
|
484
533
|
return buildSuiDeposit({
|
|
485
534
|
rpcUrl,
|
|
@@ -491,32 +540,29 @@ const suiDepositTransaction = async (client, rpcUrl, escrow, owner, quoteHash, p
|
|
|
491
540
|
quoteHash,
|
|
492
541
|
});
|
|
493
542
|
}
|
|
494
|
-
if (!
|
|
495
|
-
throw new Error("sui
|
|
496
|
-
}
|
|
497
|
-
const instructions = await client.blink.getSwapInstructions({ quoteHash });
|
|
498
|
-
if (instructions.payload.case !== "sui") {
|
|
499
|
-
throw new Error("expected sui swap instructions");
|
|
543
|
+
if (!chain.swapExecutor) {
|
|
544
|
+
throw new Error("sui source swaps are unavailable");
|
|
500
545
|
}
|
|
501
|
-
return
|
|
546
|
+
return buildSuiSourceSwapOpen({
|
|
502
547
|
rpcUrl,
|
|
503
548
|
packageId: params.routerAddress,
|
|
504
549
|
escrowObject: escrow,
|
|
550
|
+
swapExecutor: chain.swapExecutor,
|
|
505
551
|
owner,
|
|
506
552
|
srcToken: params.srcToken,
|
|
507
553
|
bridgeToken: params.bridgeTokenSrc,
|
|
508
554
|
amountIn: BigInt(params.amountIn),
|
|
509
555
|
minBridgeOut: BigInt(params.minBridgeOut),
|
|
510
556
|
quoteHash,
|
|
511
|
-
|
|
557
|
+
gasless,
|
|
512
558
|
});
|
|
513
559
|
};
|
|
514
560
|
export const createChainAdapter = (client, chain, chainId) => {
|
|
515
561
|
if (chain.vm === "svm") {
|
|
516
|
-
return new SvmChainAdapter(
|
|
562
|
+
return new SvmChainAdapter(client, chain);
|
|
517
563
|
}
|
|
518
564
|
if (chain.vm === "sui") {
|
|
519
|
-
return new SuiChainAdapter(
|
|
565
|
+
return new SuiChainAdapter(client, chain);
|
|
520
566
|
}
|
|
521
567
|
if (chainId === undefined) {
|
|
522
568
|
throw new Error(`unknown chain id for ${chain.chain}`);
|