Skip to content

Commit 3bba890

Browse files
0xCryptoZenclaude
andcommitted
feat(pyth-sui-js): migrate to @mysten/sui v2 API
Migrate @pythnetwork/pyth-sui-js and the Sui CLI tooling from @mysten/sui v1 to v2, resolving incompatibility with the new SDK. Key changes: - Replace SuiClient with ClientWithCoreApi (transport-agnostic) - Use provider.core.* namespace for all data access methods - BCS-encode dynamic field names as required by v2 - Update response handling for new object/json format - Move @mysten/sui to peerDependencies (^2.0.0) - Update CLI to use SuiJsonRpcClient and fromBase64 - Bump pyth-sui-js version from 3.0.0 to 4.0.0 Closes #3454 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 059f890 commit 3bba890

7 files changed

Lines changed: 309 additions & 139 deletions

File tree

pnpm-lock.yaml

Lines changed: 206 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/sui/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"dependencies": {
33
"@certusone/wormhole-sdk": "^0.9.12",
4-
"@mysten/sui": "^1.3.0",
4+
"@mysten/sui": "^2.0.0",
55
"@pythnetwork/contract-manager": "workspace:*",
66
"@pythnetwork/price-service-client": "^1.4.0",
77
"@pythnetwork/price-service-sdk": "^1.2.0",

target_chains/sui/cli/src/pyth_deploy.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { Transaction } from "@mysten/sui/transactions";
22

3-
import { MIST_PER_SUI, normalizeSuiObjectId, fromB64 } from "@mysten/sui/utils";
3+
import {
4+
MIST_PER_SUI,
5+
normalizeSuiObjectId,
6+
fromBase64,
7+
} from "@mysten/sui/utils";
48

59
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
610
import { execSync } from "child_process";
7-
import { SuiClient } from "@mysten/sui/client";
11+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
812
import { bcs } from "@mysten/sui/bcs";
913
import type { DataSource } from "@pythnetwork/xc-admin-common/governance_payload/SetDataSources";
1014

1115
export async function publishPackage(
1216
keypair: Ed25519Keypair,
13-
provider: SuiClient,
17+
provider: SuiJsonRpcClient,
1418
packagePath: string,
1519
): Promise<{ packageId: string; upgradeCapId: string; deployerCapId: string }> {
1620
// Build contracts
@@ -34,7 +38,9 @@ export async function publishPackage(
3438
txb.setGasBudget(MIST_PER_SUI / 2n); // 0.5 SUI
3539

3640
const [upgradeCap] = txb.publish({
37-
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
41+
modules: buildOutput.modules.map((m: string) =>
42+
Array.from(fromBase64(m)),
43+
),
3844
dependencies: buildOutput.dependencies.map((d: string) =>
3945
normalizeSuiObjectId(d),
4046
),
@@ -97,7 +103,7 @@ export async function publishPackage(
97103

98104
export async function initPyth(
99105
keypair: Ed25519Keypair,
100-
provider: SuiClient,
106+
provider: SuiJsonRpcClient,
101107
pythPackageId: string,
102108
deployerCapId: string,
103109
upgradeCapId: string,

target_chains/sui/cli/src/upgrade_pyth.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Transaction } from "@mysten/sui/transactions";
2-
import { fromB64, MIST_PER_SUI, normalizeSuiObjectId } from "@mysten/sui/utils";
3-
import { SuiClient } from "@mysten/sui/client";
2+
import {
3+
fromBase64,
4+
MIST_PER_SUI,
5+
normalizeSuiObjectId,
6+
} from "@mysten/sui/utils";
7+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
48
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
59

610
import { execSync } from "child_process";
@@ -18,7 +22,9 @@ export function buildForBytecodeAndDigest(packagePath: string) {
1822
),
1923
);
2024
return {
21-
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
25+
modules: buildOutput.modules.map((m: string) =>
26+
Array.from(fromBase64(m)),
27+
),
2228
dependencies: buildOutput.dependencies.map((d: string) =>
2329
normalizeSuiObjectId(d),
2430
),
@@ -28,7 +34,7 @@ export function buildForBytecodeAndDigest(packagePath: string) {
2834

2935
export async function upgradePyth(
3036
keypair: Ed25519Keypair,
31-
provider: SuiClient,
37+
provider: SuiJsonRpcClient,
3238
modules: number[][],
3339
dependencies: string[],
3440
signedVaa: Buffer,
@@ -78,7 +84,7 @@ export async function upgradePyth(
7884

7985
export async function migratePyth(
8086
keypair: Ed25519Keypair,
81-
provider: SuiClient,
87+
provider: SuiJsonRpcClient,
8288
signedUpgradeVaa: Buffer,
8389
contract: SuiPriceFeedContract,
8490
pythPackageOld: string,

target_chains/sui/sdk/js/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
"name": "Pyth Data Association"
44
},
55
"dependencies": {
6-
"@mysten/sui": "^1.3.0",
76
"@pythnetwork/hermes-client": "workspace:*",
87
"buffer": "^6.0.3"
98
},
9+
"peerDependencies": {
10+
"@mysten/sui": "^2.0.0"
11+
},
1012
"description": "Pyth Network Sui Utilities",
1113
"devDependencies": {
14+
"@mysten/sui": "^2.0.0",
1215
"@pythnetwork/jest-config": "workspace:",
1316
"@truffle/hdwallet-provider": "^2.1.5",
1417
"@types/ethereum-protocol": "^1.0.2",
@@ -86,5 +89,5 @@
8689
},
8790
"type": "module",
8891
"types": "./dist/cjs/index.d.ts",
89-
"version": "3.0.0"
90-
}
92+
"version": "4.0.0"
93+
}

target_chains/sui/sdk/js/src/client.ts

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { Buffer } from "node:buffer";
66

77
import { bcs } from "@mysten/sui/bcs";
8-
import { SuiClient } from "@mysten/sui/client";
8+
import type { ClientWithCoreApi } from "@mysten/sui/client";
99
import { Transaction } from "@mysten/sui/transactions";
1010
import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils";
1111
import type { HexString } from "@pythnetwork/hermes-client";
@@ -24,7 +24,7 @@ export class SuiPythClient {
2424
private priceFeedObjectIdCache = new Map<HexString, ObjectId>();
2525
private baseUpdateFee: number | undefined;
2626
constructor(
27-
public provider: SuiClient,
27+
public provider: ClientWithCoreApi,
2828
public pythStateId: ObjectId,
2929
public wormholeStateId: ObjectId,
3030
) {
@@ -34,18 +34,14 @@ export class SuiPythClient {
3434

3535
async getBaseUpdateFee(): Promise<number> {
3636
if (this.baseUpdateFee === undefined) {
37-
const result = await this.provider.getObject({
38-
id: this.pythStateId,
39-
options: { showContent: true },
37+
const result = await this.provider.core.getObject({
38+
objectId: this.pythStateId,
39+
include: { json: true },
4040
});
41-
if (
42-
!result.data?.content ||
43-
result.data.content.dataType !== "moveObject"
44-
)
41+
const json = result.object.json as Record<string, unknown> | null;
42+
if (!json)
4543
throw new Error("Unable to fetch pyth state object");
46-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
47-
// @ts-ignore
48-
this.baseUpdateFee = result.data.content.fields.base_update_fee as number;
44+
this.baseUpdateFee = Number(json.base_update_fee);
4945
}
5046

5147
return this.baseUpdateFee;
@@ -58,27 +54,23 @@ export class SuiPythClient {
5854
* @returns package id
5955
*/
6056
async getPackageId(objectId: ObjectId): Promise<ObjectId> {
61-
const state = await this.provider
62-
.getObject({
63-
id: objectId,
64-
options: {
65-
showContent: true,
66-
},
67-
})
68-
.then((result) => {
69-
if (result.data?.content?.dataType == "moveObject") {
70-
return result.data.content.fields;
71-
}
72-
console.log(result.data?.content);
73-
74-
throw new Error(`Cannot fetch package id for object ${objectId}`);
75-
});
57+
const result = await this.provider.core.getObject({
58+
objectId,
59+
include: { json: true },
60+
});
61+
const json = result.object.json as Record<string, unknown> | null;
62+
if (!json) {
63+
throw new Error(`Cannot fetch package id for object ${objectId}`);
64+
}
7665

77-
if ("upgrade_cap" in state) {
78-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
79-
// @ts-ignore
80-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
81-
return state.upgrade_cap.fields.package;
66+
if ("upgrade_cap" in json) {
67+
const upgradeCap = json.upgrade_cap as Record<string, unknown>;
68+
// JSON-RPC wraps nested objects in { type, fields }, gRPC may not.
69+
const fields = (upgradeCap.fields ?? upgradeCap) as Record<
70+
string,
71+
unknown
72+
>;
73+
return fields.package as string;
8274
}
8375

8476
throw new Error("upgrade_cap not found");
@@ -299,28 +291,34 @@ export class SuiPythClient {
299291
const normalizedFeedId = feedId.replace("0x", "");
300292
if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) {
301293
const { id: tableId, fieldType } = await this.getPriceTableInfo();
302-
const result = await this.provider.getDynamicFieldObject({
303-
parentId: tableId,
304-
name: {
305-
type: `${fieldType}::price_identifier::PriceIdentifier`,
306-
value: {
307-
bytes: [...Buffer.from(normalizedFeedId, "hex")],
294+
// BCS-encode the PriceIdentifier struct name.
295+
// PriceIdentifier has a single field `bytes: vector<u8>`, so its BCS
296+
// encoding is the same as just encoding the inner vector<u8>.
297+
const nameBcs = bcs
298+
.vector(bcs.U8)
299+
.serialize([...Buffer.from(normalizedFeedId, "hex")])
300+
.toBytes();
301+
try {
302+
const result = await this.provider.core.getDynamicObjectField({
303+
parentId: tableId,
304+
name: {
305+
type: `${fieldType}::price_identifier::PriceIdentifier`,
306+
bcs: nameBcs,
308307
},
309-
},
310-
});
311-
if (!result.data?.content) {
308+
include: { json: true },
309+
});
310+
const json = result.object.json as Record<string, unknown> | null;
311+
if (!json) {
312+
return undefined;
313+
}
314+
this.priceFeedObjectIdCache.set(
315+
normalizedFeedId,
316+
json.value as string,
317+
);
318+
} catch {
319+
// Dynamic field not found
312320
return undefined;
313321
}
314-
if (result.data.content.dataType !== "moveObject") {
315-
throw new Error("Price feed type mismatch");
316-
}
317-
this.priceFeedObjectIdCache.set(
318-
normalizedFeedId,
319-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
320-
// @ts-ignore
321-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
322-
result.data.content.fields.value,
323-
);
324322
}
325323
return this.priceFeedObjectIdCache.get(normalizedFeedId);
326324
}
@@ -331,24 +329,29 @@ export class SuiPythClient {
331329
*/
332330
async getPriceTableInfo(): Promise<{ id: ObjectId; fieldType: ObjectId }> {
333331
if (this.priceTableInfo === undefined) {
334-
const result = await this.provider.getDynamicFieldObject({
332+
// BCS-encode the "price_info" name as vector<u8>
333+
const nameBcs = bcs
334+
.vector(bcs.U8)
335+
.serialize([...Buffer.from("price_info")])
336+
.toBytes();
337+
const result = await this.provider.core.getDynamicObjectField({
335338
parentId: this.pythStateId,
336339
name: {
337340
type: "vector<u8>",
338-
value: "price_info",
341+
bcs: nameBcs,
339342
},
340343
});
341-
if (!result.data?.type) {
344+
if (!result.object.type) {
342345
throw new Error(
343346
"Price Table not found, contract may not be initialized",
344347
);
345348
}
346-
let type = result.data.type.replace("0x2::table::Table<", "");
349+
let type = result.object.type.replace("0x2::table::Table<", "");
347350
type = type.replace(
348351
"::price_identifier::PriceIdentifier, 0x2::object::ID>",
349352
"",
350353
);
351-
this.priceTableInfo = { id: result.data.objectId, fieldType: type };
354+
this.priceTableInfo = { id: result.object.objectId, fieldType: type };
352355
}
353356
return this.priceTableInfo;
354357
}

target_chains/sui/sdk/js/src/examples/SuiRelay.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import yargs from "yargs";
22
import { hideBin } from "yargs/helpers";
3-
import { SuiClient } from "@mysten/sui/client";
3+
import { SuiGrpcClient } from "@mysten/sui/grpc";
44
import { Transaction } from "@mysten/sui/transactions";
55
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
66

@@ -36,10 +36,15 @@ const argvPromise = yargs(hideBin(process.argv))
3636
description: "Wormhole state object id.",
3737
type: "string",
3838
demandOption: true,
39+
})
40+
.option("network", {
41+
description: "Sui network (mainnet, testnet, devnet, localnet)",
42+
type: "string",
43+
default: "testnet",
3944
}).argv;
4045

41-
export function getProvider(url: string) {
42-
return new SuiClient({ url });
46+
export function getProvider(url: string, network: string) {
47+
return new SuiGrpcClient({ baseUrl: url, network });
4348
}
4449
async function run() {
4550
if (process.env.SUI_KEY === undefined) {
@@ -55,7 +60,7 @@ async function run() {
5560
throw new Error("Not a valid input!");
5661
}
5762

58-
const provider = getProvider(argv["full-node"]);
63+
const provider = getProvider(argv["full-node"], argv["network"]);
5964
const wormholeStateId = argv["wormhole-state-id"];
6065
const pythStateId = argv["pyth-state-id"];
6166

@@ -90,15 +95,16 @@ async function run() {
9095
Buffer.from(process.env.SUI_KEY, "hex"),
9196
);
9297
tx.setGasBudget(1000000);
93-
const result = await provider.signAndExecuteTransaction({
98+
const result = await provider.core.signAndExecuteTransaction({
9499
signer: wallet,
95100
transaction: tx,
96-
options: {
97-
showEffects: true,
98-
showEvents: true,
101+
include: {
102+
effects: true,
103+
events: true,
99104
},
100105
});
101-
console.dir(result, { depth: null });
106+
const txResult = result.Transaction ?? result.FailedTransaction;
107+
console.dir(txResult, { depth: null });
102108
}
103109

104110
run();

0 commit comments

Comments
 (0)