Skip to content

Commit 18e8b8d

Browse files
cctdanielcprussin
andauthored
feat(contract_manager): add support for fuel (#1656)
* add initial FuelChain to chains.ts * precommit * remove unused code * update chains * remove generated abis for debug/ and deployments/ * update fuel contract * add FuelWormholeContract * fix fuel contract guardian set bug * add FuelPriceFeedContract * precommit * remove console.log * add fuel types * precommit * fix fuel contract assertion * update testnet contract address * update testnet contract address * feat(target_chains/fuel): add minimal fuel js sdk (#1690) * add fuel js sdk * precommit * use pyth-fuel-js package * update testnet contracts * remove unused script * update lock file * update lockfile * update lockfile * fix lint * Use `copyfiles` to ensure declarations in src get copied to lib (#1698) * address comments --------- Co-authored-by: Connor Prussin <[email protected]>
1 parent ccdeaa8 commit 18e8b8d

File tree

29 files changed

+101486
-1900
lines changed

29 files changed

+101486
-1900
lines changed

contract_manager/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@pythnetwork/entropy-sdk-solidity": "workspace:*",
3434
"@pythnetwork/price-service-client": "workspace:*",
3535
"@pythnetwork/pyth-sdk-solidity": "workspace:^",
36+
"@pythnetwork/pyth-fuel-js": "workspace:*",
3637
"@pythnetwork/pyth-sui-js": "workspace:*",
3738
"@pythnetwork/solana-utils": "workspace:^",
3839
"@pythnetwork/xc-admin-common": "workspace:*",
@@ -41,6 +42,9 @@
4142
"aptos": "^1.5.0",
4243
"axios": "^0.24.0",
4344
"bs58": "^5.0.0",
45+
"extract-files": "^13.0.0",
46+
"fuels": "^0.89.2",
47+
"ramda": "^0.30.1",
4448
"ts-node": "^10.9.1",
4549
"typescript": "^5.3.3",
4650
"web3": "^1.8.2",

contract_manager/src/chains.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { Network } from "@injectivelabs/networks";
2222
import { SuiClient } from "@mysten/sui.js/client";
2323
import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
2424
import { TokenId } from "./token";
25+
import { BN, Provider, Wallet, WalletUnlocked } from "fuels";
26+
import { FUEL_ETH_ASSET_ID } from "@pythnetwork/pyth-fuel-js";
2527

2628
export type ChainConfig = Record<string, string> & {
2729
mainnet: boolean;
@@ -560,3 +562,71 @@ export class AptosChain extends Chain {
560562
return { id: result.hash, info: result };
561563
}
562564
}
565+
566+
export class FuelChain extends Chain {
567+
static type = "FuelChain";
568+
569+
constructor(
570+
id: string,
571+
mainnet: boolean,
572+
wormholeChainName: string,
573+
nativeToken: TokenId | undefined,
574+
public gqlUrl: string
575+
) {
576+
super(id, mainnet, wormholeChainName, nativeToken);
577+
}
578+
579+
async getProvider(): Promise<Provider> {
580+
return await Provider.create(this.gqlUrl);
581+
}
582+
583+
async getWallet(privateKey: PrivateKey): Promise<WalletUnlocked> {
584+
const provider = await this.getProvider();
585+
return Wallet.fromPrivateKey(privateKey, provider);
586+
}
587+
588+
/**
589+
* Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
590+
* @param digest hex string of the 32 byte digest for the new package without the 0x prefix
591+
*/
592+
generateGovernanceUpgradePayload(digest: string): Buffer {
593+
// This might throw an error because the Fuel contract doesn't support upgrades yet (blocked on Fuel releasing Upgradeability standard)
594+
return new UpgradeContract256Bit(this.wormholeChainName, digest).encode();
595+
}
596+
597+
getType(): string {
598+
return FuelChain.type;
599+
}
600+
601+
toJson(): KeyValueConfig {
602+
return {
603+
id: this.id,
604+
wormholeChainName: this.wormholeChainName,
605+
mainnet: this.mainnet,
606+
gqlUrl: this.gqlUrl,
607+
type: FuelChain.type,
608+
};
609+
}
610+
611+
static fromJson(parsed: ChainConfig): FuelChain {
612+
if (parsed.type !== FuelChain.type) throw new Error("Invalid type");
613+
return new FuelChain(
614+
parsed.id,
615+
parsed.mainnet,
616+
parsed.wormholeChainName,
617+
parsed.nativeToken,
618+
parsed.gqlUrl
619+
);
620+
}
621+
622+
async getAccountAddress(privateKey: PrivateKey): Promise<string> {
623+
const wallet = await this.getWallet(privateKey);
624+
return wallet.address.toString();
625+
}
626+
627+
async getAccountBalance(privateKey: PrivateKey): Promise<number> {
628+
const wallet = await this.getWallet(privateKey);
629+
const balance: BN = await wallet.getBalance(FUEL_ETH_ASSET_ID);
630+
return Number(balance) / 10 ** 9;
631+
}
632+
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { Chain, FuelChain } from "../chains";
2+
import { WormholeContract } from "./wormhole";
3+
import {
4+
PYTH_CONTRACT_ABI as FuelContractAbi,
5+
FUEL_ETH_ASSET_ID,
6+
PriceFeedOutput,
7+
DataSourceOutput,
8+
} from "@pythnetwork/pyth-fuel-js";
9+
10+
import {
11+
Account,
12+
Contract,
13+
Wallet,
14+
arrayify,
15+
hexlify,
16+
InvocationCallResult,
17+
} from "fuels";
18+
import { PriceFeed, PriceFeedContract, PrivateKey, TxResult } from "../base";
19+
20+
import { TokenQty } from "../token";
21+
import { DataSource } from "@pythnetwork/xc-admin-common";
22+
23+
export class FuelWormholeContract extends WormholeContract {
24+
static type = "FuelWormholeContract";
25+
26+
getId(): string {
27+
return `${this.chain.getId()}_${this.address}_${FuelWormholeContract.type}`;
28+
}
29+
30+
getType(): string {
31+
return FuelWormholeContract.type;
32+
}
33+
34+
toJson() {
35+
return {
36+
chain: this.chain.getId(),
37+
address: this.address,
38+
type: FuelWormholeContract.type,
39+
};
40+
}
41+
42+
static fromJson(
43+
chain: Chain,
44+
parsed: {
45+
type: string;
46+
address: string;
47+
}
48+
): FuelWormholeContract {
49+
if (parsed.type !== FuelWormholeContract.type)
50+
throw new Error("Invalid type");
51+
if (!(chain instanceof FuelChain))
52+
throw new Error(`Wrong chain type ${chain}`);
53+
return new FuelWormholeContract(chain, parsed.address);
54+
}
55+
56+
constructor(public chain: FuelChain, public address: string) {
57+
super();
58+
}
59+
60+
async getContract(wallet?: Wallet): Promise<Contract> {
61+
const provider = await this.chain.getProvider();
62+
63+
return new Contract(
64+
this.address,
65+
FuelContractAbi,
66+
wallet ? (wallet as Account) : provider
67+
);
68+
}
69+
70+
async getCurrentGuardianSetIndex(): Promise<number> {
71+
const contract = await this.getContract();
72+
const guardianSetIndex = (
73+
await contract.functions.current_guardian_set_index().get()
74+
).value;
75+
return guardianSetIndex;
76+
}
77+
78+
async getChainId(): Promise<number> {
79+
const contract = await this.getContract();
80+
const chainId = (await contract.functions.chain_id().get()).value;
81+
return chainId;
82+
}
83+
84+
async getGuardianSet(): Promise<string[]> {
85+
const contract = await this.getContract();
86+
const guardianSetIndex = await this.getCurrentGuardianSetIndex();
87+
const guardianSet = (
88+
await contract.functions.guardian_set(guardianSetIndex).get()
89+
).value;
90+
return guardianSet;
91+
}
92+
93+
async upgradeGuardianSets(
94+
senderPrivateKey: PrivateKey,
95+
vaa: Buffer
96+
): Promise<TxResult> {
97+
const wallet = await this.chain.getWallet(senderPrivateKey);
98+
const contract = await this.getContract(wallet);
99+
const tx = await contract.functions
100+
.submit_new_guardian_set(arrayify(vaa))
101+
.call(); // you might get `Error updating Guardianset for fuel_testnet_{address} TypeError: response.body.getReader is not a function` but the tx could still be successful, this is due to fuels using native fetch but some other packages in the monorepo is using node-fetch which overrides the fetch here
102+
return {
103+
id: tx.transactionId,
104+
info: JSON.stringify(tx.transactionResponse),
105+
};
106+
}
107+
}
108+
109+
export class FuelPriceFeedContract extends PriceFeedContract {
110+
static type = "FuelPriceFeedContract";
111+
112+
constructor(public chain: FuelChain, public address: string) {
113+
super();
114+
}
115+
116+
static fromJson(
117+
chain: Chain,
118+
parsed: { type: string; address: string }
119+
): FuelPriceFeedContract {
120+
if (parsed.type !== FuelPriceFeedContract.type)
121+
throw new Error("Invalid type");
122+
if (!(chain instanceof FuelChain))
123+
throw new Error(`Wrong chain type ${chain}`);
124+
return new FuelPriceFeedContract(chain, parsed.address);
125+
}
126+
127+
getId(): string {
128+
return `${this.chain.getId()}_${this.address}_${
129+
FuelPriceFeedContract.type
130+
}`;
131+
}
132+
133+
getType(): string {
134+
return FuelPriceFeedContract.type;
135+
}
136+
137+
async getContract(wallet?: Wallet): Promise<Contract> {
138+
const provider = await this.chain.getProvider();
139+
140+
return new Contract(
141+
this.address,
142+
FuelContractAbi,
143+
wallet ? (wallet as Account) : provider
144+
);
145+
}
146+
147+
async getTotalFee(): Promise<TokenQty> {
148+
const contract = await this.getContract();
149+
const balance = await contract.getBalance(this.address);
150+
return {
151+
amount: BigInt(balance.toString()),
152+
denom: this.chain.getNativeToken(),
153+
};
154+
}
155+
156+
async getLastExecutedGovernanceSequence() {
157+
const pythContract = await this.getContract();
158+
return Number(
159+
(await pythContract.functions.last_executed_governance_sequence().get())
160+
.value
161+
);
162+
}
163+
164+
async getPriceFeed(feedId: string): Promise<PriceFeed | undefined> {
165+
const pythContract = await this.getContract();
166+
const feed = "0x" + feedId;
167+
const exists = (
168+
await pythContract.functions.price_feed_exists(hexlify(feed)).get()
169+
).value;
170+
if (!exists) {
171+
return undefined;
172+
}
173+
const priceFeed: PriceFeedOutput = (
174+
await pythContract.functions.price_feed_unsafe(feed).get()
175+
).value;
176+
return {
177+
price: {
178+
price: priceFeed.price.price.toString(),
179+
conf: priceFeed.price.confidence.toString(),
180+
expo: priceFeed.price.exponent.toString(),
181+
publishTime: priceFeed.price.publish_time.toString(),
182+
},
183+
emaPrice: {
184+
price: priceFeed.ema_price.price.toString(),
185+
conf: priceFeed.ema_price.confidence.toString(),
186+
expo: priceFeed.ema_price.exponent.toString(),
187+
publishTime: priceFeed.ema_price.publish_time.toString(),
188+
},
189+
};
190+
}
191+
192+
async getValidTimePeriod() {
193+
const pythContract = await this.getContract();
194+
const validTimePeriod = (
195+
await pythContract.functions.valid_time_period().get()
196+
).value;
197+
return Number(validTimePeriod);
198+
}
199+
200+
/**
201+
* Returns the wormhole contract which is being used for VAA verification
202+
*/
203+
async getWormholeContract(): Promise<FuelWormholeContract> {
204+
// price feed contract and wormhole contract lives on the same address in fuel
205+
return new FuelWormholeContract(this.chain, this.address);
206+
}
207+
208+
async getBaseUpdateFee() {
209+
const pythContract = await this.getContract();
210+
const amount = (await pythContract.functions.single_update_fee().get())
211+
.value;
212+
return {
213+
amount: amount.toString(),
214+
denom: this.chain.getNativeToken(),
215+
};
216+
}
217+
218+
async getDataSources(): Promise<DataSource[]> {
219+
const pythContract = await this.getContract();
220+
const result: InvocationCallResult<DataSourceOutput[]> =
221+
await pythContract.functions.valid_data_sources().get();
222+
return result.value.map(
223+
({
224+
chain_id,
225+
emitter_address,
226+
}: {
227+
chain_id: number;
228+
emitter_address: string;
229+
}) => {
230+
return {
231+
emitterChain: chain_id,
232+
emitterAddress: emitter_address.replace("0x", ""),
233+
};
234+
}
235+
);
236+
}
237+
238+
async getGovernanceDataSource(): Promise<DataSource> {
239+
const pythContract = await this.getContract();
240+
const result: InvocationCallResult<DataSourceOutput> =
241+
await pythContract.functions.governance_data_source().get();
242+
return {
243+
emitterChain: result.value.chain_id,
244+
emitterAddress: result.value.emitter_address.replace("0x", ""),
245+
};
246+
}
247+
248+
async executeUpdatePriceFeed(senderPrivateKey: PrivateKey, vaas: Buffer[]) {
249+
const wallet = await this.chain.getWallet(senderPrivateKey);
250+
const contract = await this.getContract(wallet);
251+
const priceFeedUpdateData = vaas.map((vaa) => new Uint8Array(vaa));
252+
const updateFee: number = (
253+
await contract.functions.update_fee(priceFeedUpdateData).get()
254+
).value;
255+
const tx = await contract.functions
256+
.update_price_feeds(priceFeedUpdateData)
257+
.callParams({
258+
forward: [updateFee, hexlify(FUEL_ETH_ASSET_ID)],
259+
})
260+
.call();
261+
262+
return { id: tx.transactionId, info: tx.transactionResponse };
263+
}
264+
265+
async executeGovernanceInstruction(
266+
senderPrivateKey: PrivateKey,
267+
vaa: Buffer
268+
) {
269+
const wallet = await this.chain.getWallet(senderPrivateKey);
270+
const contract = await this.getContract(wallet);
271+
const tx = await contract.functions
272+
.execute_governance_instruction(arrayify(vaa))
273+
.call();
274+
return { id: tx.transactionId, info: tx.transactionResponse };
275+
}
276+
277+
getChain(): FuelChain {
278+
return this.chain;
279+
}
280+
281+
toJson() {
282+
return {
283+
chain: this.chain.getId(),
284+
address: this.address,
285+
type: FuelPriceFeedContract.type,
286+
};
287+
}
288+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./aptos";
22
export * from "./cosmwasm";
33
export * from "./evm";
4+
export * from "./fuel";
45
export * from "./sui";
56
export * from "./wormhole";

0 commit comments

Comments
 (0)