Skip to content

Commit 7523c69

Browse files
committed
feat: fastusdc bridge provider
1 parent 7440de4 commit 7523c69

File tree

14 files changed

+1167
-6
lines changed

14 files changed

+1167
-6
lines changed

packages/bridge/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@0xsquid/sdk": "^1.14.0",
29+
"@agoric/cosmic-proto": "^0.5.0-u20.0",
2930
"@axelar-network/axelarjs-sdk": "0.17.0",
3031
"@cosmjs/encoding": "0.32.3",
3132
"@cosmjs/proto-signing": "0.32.3",
@@ -36,6 +37,7 @@
3637
"@osmosis-labs/unit": "0.10.24-ibc.go.v7.hot.fix",
3738
"@osmosis-labs/utils": "^1.0.0",
3839
"base-x": "^5.0.0",
40+
"big-integer": "^1.6.48",
3941
"cachified": "^3.5.4",
4042
"launchdarkly-node-client-sdk": "^3.3.0",
4143
"long": "^5.2.3",

packages/bridge/src/bridge-providers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AxelarBridgeProvider } from "./axelar";
2+
import { FastUsdcBridgeProvider } from "./fast-usdc";
23
import { IbcBridgeProvider } from "./ibc";
34
import { Int3faceBridgeProvider } from "./int3face";
45
import { BridgeProviderContext } from "./interface";
@@ -25,6 +26,7 @@ export class BridgeProviders {
2526
[PicassoBridgeProvider.ID]: PicassoBridgeProvider;
2627
[PenumbraBridgeProvider.ID]: PenumbraBridgeProvider;
2728
[Int3faceBridgeProvider.ID]: Int3faceBridgeProvider;
29+
[FastUsdcBridgeProvider.ID]: FastUsdcBridgeProvider;
2830
};
2931

3032
constructor(integratorId: string, commonContext: BridgeProviderContext) {
@@ -46,6 +48,7 @@ export class BridgeProviders {
4648
[PicassoBridgeProvider.ID]: new PicassoBridgeProvider(commonContext),
4749
[PenumbraBridgeProvider.ID]: new PenumbraBridgeProvider(commonContext),
4850
[Int3faceBridgeProvider.ID]: new Int3faceBridgeProvider(commonContext),
51+
[FastUsdcBridgeProvider.ID]: new FastUsdcBridgeProvider(commonContext),
4952
};
5053
}
5154
}
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import { apiClient } from "@osmosis-labs/utils";
2+
import bigInteger from "big-integer";
3+
import cachified from "cachified";
4+
5+
import { BridgeProviderContext } from "../interface";
6+
7+
export const AGORIC_API_URL = "https://main.api.agoric.net";
8+
export const NOBLE_TO_AGORIC_CHANNEL = "channel-21";
9+
export const NETWORK_CONFIG_URL = "https://main.agoric.net/network-config";
10+
export const FAST_USDC_SERVICE_URL =
11+
"https://fastusdc-map.agoric-core.workers.dev/store";
12+
export const NOBLE_API_URL = "https://noble-api.polkachu.com";
13+
14+
export interface FastUsdcFee {
15+
numerator: string;
16+
denominator: string;
17+
flatPortion: string;
18+
}
19+
20+
export interface FastUsdcChainPolicy {
21+
chainId?: number;
22+
rateLimits?: {
23+
tx?: {
24+
digits?: string;
25+
};
26+
};
27+
}
28+
29+
export interface FastUsdcChainPolicies {
30+
[chainName: string]: FastUsdcChainPolicy;
31+
}
32+
33+
/** Fetches data from Agoric API endpoints related to Fast USDC. */
34+
export class FastUsdcClient {
35+
constructor(protected readonly ctx: BridgeProviderContext) {}
36+
37+
/**
38+
* Fetches the Fast USDC fee configuration.
39+
*/
40+
async getFeeConfig(): Promise<FastUsdcFee> {
41+
return cachified({
42+
cache: this.ctx.cache,
43+
key: "FastUsdcFeeConfig",
44+
ttl:
45+
process.env.NODE_ENV === "test" ||
46+
process.env.NODE_ENV === "development"
47+
? -1
48+
: 60 * 1000, // 60 seconds
49+
getFreshValue: async (): Promise<FastUsdcFee> => {
50+
try {
51+
const rawData = await apiClient<{ value: string }>(
52+
`${AGORIC_API_URL}/agoric/vstorage/data/published.fastUsdc.feeConfig`
53+
);
54+
55+
const nestedValue = JSON.parse(rawData.value);
56+
const latestValue = nestedValue?.values?.at(-1);
57+
if (!latestValue)
58+
throw new Error(
59+
"Could not find latest value in feeConfig response"
60+
);
61+
const parsedBody = JSON.parse(JSON.parse(latestValue).body.slice(1));
62+
63+
const feeConfig = {
64+
numerator: parsedBody?.variableRate?.numerator?.value,
65+
denominator: parsedBody?.variableRate?.denominator?.value,
66+
flatPortion: parsedBody?.flat?.value,
67+
};
68+
69+
if (
70+
typeof feeConfig.numerator !== "string" ||
71+
typeof feeConfig.denominator !== "string" ||
72+
typeof feeConfig.flatPortion !== "string"
73+
) {
74+
throw new Error(
75+
"Invalid fee config structure parsed from Agoric API"
76+
);
77+
}
78+
79+
return feeConfig;
80+
} catch (e) {
81+
console.error("Failed to fetch or parse Fast USDC fee config:", e);
82+
throw new Error("Failed to fetch or parse Fast USDC fee config");
83+
}
84+
},
85+
});
86+
}
87+
88+
/**
89+
* Fetches the Fast USDC pool balance (available amount).
90+
*/
91+
async getPoolBalance(): Promise<bigInteger.BigInteger> {
92+
return cachified({
93+
cache: this.ctx.cache,
94+
key: "FastUsdcPoolBalance",
95+
ttl:
96+
process.env.NODE_ENV === "test" ||
97+
process.env.NODE_ENV === "development"
98+
? -1
99+
: 10 * 1000, // 10 seconds
100+
getFreshValue: async (): Promise<bigInteger.BigInteger> => {
101+
try {
102+
const rawData = await apiClient<{ value: string }>(
103+
`${AGORIC_API_URL}/agoric/vstorage/data/published.fastUsdc.poolMetrics`
104+
);
105+
106+
const nestedValue = JSON.parse(rawData.value);
107+
const latestValue = nestedValue?.values?.at(-1);
108+
if (!latestValue)
109+
throw new Error(
110+
"Could not find latest value in poolMetrics response"
111+
);
112+
const poolMetrics = JSON.parse(JSON.parse(latestValue).body.slice(1));
113+
114+
const totalPoolBalanceStr = poolMetrics?.shareWorth?.numerator?.value;
115+
const encumberedBalanceStr = poolMetrics?.encumberedBalance?.value;
116+
117+
const totalPoolBalance = bigInteger(totalPoolBalanceStr);
118+
const encumberedBalance = bigInteger(encumberedBalanceStr);
119+
const availableBalance = totalPoolBalance
120+
.minus(encumberedBalance)
121+
.minus(1);
122+
123+
return availableBalance.isNegative()
124+
? bigInteger(0)
125+
: availableBalance;
126+
} catch (e) {
127+
console.error("Failed to fetch or parse Fast USDC pool balance:", e);
128+
throw new Error("Failed to fetch or parse Fast USDC pool balance");
129+
}
130+
},
131+
});
132+
}
133+
134+
/**
135+
* Fetches the chain policies for Fast USDC transfers.
136+
*/
137+
async getChainPolicies(): Promise<FastUsdcChainPolicies> {
138+
return cachified({
139+
cache: this.ctx.cache,
140+
key: "FastUsdcChainPolicies",
141+
ttl:
142+
process.env.NODE_ENV === "test" ||
143+
process.env.NODE_ENV === "development"
144+
? -1
145+
: 20 * 1000, // 20 seconds
146+
getFreshValue: async (): Promise<FastUsdcChainPolicies> => {
147+
try {
148+
const rawData = await apiClient<{ value: string }>(
149+
`${AGORIC_API_URL}/agoric/vstorage/data/published.fastUsdc.feedPolicy`
150+
);
151+
152+
const nestedValue = JSON.parse(rawData.value);
153+
const latestValue = nestedValue?.values?.at(-1);
154+
if (!latestValue)
155+
throw new Error(
156+
"Could not find latest value in feedPolicy response"
157+
);
158+
const parsedBody = JSON.parse(JSON.parse(latestValue).body);
159+
160+
const chainPolicies = parsedBody?.chainPolicies;
161+
162+
if (typeof chainPolicies !== "object" || chainPolicies === null) {
163+
throw new Error(
164+
"Invalid chain policies structure parsed from Agoric API"
165+
);
166+
}
167+
168+
return chainPolicies as FastUsdcChainPolicies;
169+
} catch (e) {
170+
console.error(
171+
"Failed to fetch or parse Fast USDC chain policies:",
172+
e
173+
);
174+
throw new Error("Failed to fetch or parse Fast USDC chain policies");
175+
}
176+
},
177+
});
178+
}
179+
180+
/**
181+
* Checks if Fast USDC is allowed in the network configuration.
182+
*/
183+
async isAllowedInNetworkConfig(): Promise<boolean> {
184+
return cachified({
185+
cache: this.ctx.cache,
186+
key: "FastUsdcAllowedInNetworkConfig",
187+
ttl:
188+
process.env.NODE_ENV === "test" ||
189+
process.env.NODE_ENV === "development"
190+
? -1
191+
: 10 * 1000, // 10 seconds
192+
getFreshValue: async (): Promise<boolean> => {
193+
try {
194+
const config = await apiClient<any>(NETWORK_CONFIG_URL);
195+
196+
const fastUsdcAllowed = config?.fastUsdcAllowed;
197+
198+
if (fastUsdcAllowed === undefined) {
199+
console.error(
200+
'Could not find key "fastUsdcAllowed" in network config, disabling feature.',
201+
NETWORK_CONFIG_URL,
202+
config
203+
);
204+
return false;
205+
}
206+
207+
if (!fastUsdcAllowed) {
208+
console.warn(
209+
"Fast USDC is not allowed in network config, disabling feature.",
210+
NETWORK_CONFIG_URL,
211+
config
212+
);
213+
}
214+
215+
return !!fastUsdcAllowed;
216+
} catch (e) {
217+
console.error(
218+
"Failed to fetch network config for Fast USDC allowance:",
219+
e
220+
);
221+
return false;
222+
}
223+
},
224+
});
225+
}
226+
227+
/**
228+
* Gets the encoded agoric destination address for the given user destination address,
229+
* has a side effect of POSTing the address to the Fast USDC service when this function is called.
230+
*/
231+
async getSkipRouteDestinationAddress(
232+
userDestinationAddress: string
233+
): Promise<string> {
234+
return cachified({
235+
cache: this.ctx.cache,
236+
key: JSON.stringify({
237+
id: "FastUsdcSkipRouteDestinationAddress",
238+
address: userDestinationAddress,
239+
}),
240+
ttl:
241+
process.env.NODE_ENV === "test" ||
242+
process.env.NODE_ENV === "development"
243+
? -1
244+
: Infinity,
245+
getFreshValue: async (): Promise<string> => {
246+
const { encodeAddressHook } = await import(
247+
"@agoric/cosmic-proto/address-hooks.js"
248+
);
249+
250+
const vstorage = await fetch(
251+
`${AGORIC_API_URL}/agoric/vstorage/data/published.fastUsdc`
252+
);
253+
const data = await vstorage.json();
254+
const settlementAccountAddress = JSON.parse(
255+
JSON.parse(data.value).values.at(-1)
256+
).settlementAccount;
257+
const encodedAgoricAddress = encodeAddressHook(
258+
settlementAccountAddress,
259+
{
260+
EUD: userDestinationAddress,
261+
}
262+
);
263+
await fetch(FAST_USDC_SERVICE_URL, {
264+
method: "POST",
265+
headers: [["Content-Type", "application/json"]],
266+
body: JSON.stringify({
267+
channel: NOBLE_TO_AGORIC_CHANNEL,
268+
recipient: encodedAgoricAddress,
269+
}),
270+
mode: "no-cors",
271+
});
272+
return encodedAgoricAddress;
273+
},
274+
});
275+
}
276+
277+
async getNobleForwardingAddress(agoricAddress: string): Promise<string> {
278+
return cachified({
279+
cache: this.ctx.cache,
280+
key: JSON.stringify({
281+
id: "FastUsdcNobleForwardingAddress",
282+
address: agoricAddress,
283+
}),
284+
ttl:
285+
process.env.NODE_ENV === "test" ||
286+
process.env.NODE_ENV === "development"
287+
? -1
288+
: Infinity,
289+
getFreshValue: async (): Promise<string> => {
290+
const client = await apiClient<{ address: string; exists: boolean }>(
291+
NOBLE_API_URL +
292+
"/noble/forwarding/v1/address/" +
293+
NOBLE_TO_AGORIC_CHANNEL +
294+
"/" +
295+
agoricAddress +
296+
"/"
297+
);
298+
return client.address;
299+
},
300+
});
301+
}
302+
}

0 commit comments

Comments
 (0)