@ledgerhq/coin-sui 0.10.0 → 0.10.1-nightly.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/CHANGELOG.md +11 -0
- package/index.d.ts +1 -0
- package/jest.config.js +1 -1
- package/lib/api/index.integration.test.js +2 -2
- package/lib/api/index.integration.test.js.map +1 -1
- package/lib/bridge/buildTransaction.d.ts +1 -1
- package/lib/bridge/buildTransaction.d.ts.map +1 -1
- package/lib/bridge/buildTransaction.integration.test.js +12 -0
- package/lib/bridge/buildTransaction.integration.test.js.map +1 -1
- package/lib/bridge/buildTransaction.js +8 -4
- package/lib/bridge/buildTransaction.js.map +1 -1
- package/lib/bridge/buildTransaction.test.js +14 -10
- package/lib/bridge/buildTransaction.test.js.map +1 -1
- package/lib/bridge/getFeesForTransaction.d.ts.map +1 -1
- package/lib/bridge/getFeesForTransaction.js +2 -3
- package/lib/bridge/getFeesForTransaction.js.map +1 -1
- package/lib/bridge/signOperation.integration.test.js +2 -2
- package/lib/bridge/signOperation.integration.test.js.map +1 -1
- package/lib/logic/craftTransaction.js +1 -1
- package/lib/logic/craftTransaction.js.map +1 -1
- package/lib/network/sdk.d.ts +18 -3
- package/lib/network/sdk.d.ts.map +1 -1
- package/lib/network/sdk.integration.test.js +2 -2
- package/lib/network/sdk.integration.test.js.map +1 -1
- package/lib/network/sdk.js +64 -51
- package/lib/network/sdk.js.map +1 -1
- package/lib/network/sdk.test.js +148 -65
- package/lib/network/sdk.test.js.map +1 -1
- package/lib-es/api/index.integration.test.js +2 -2
- package/lib-es/api/index.integration.test.js.map +1 -1
- package/lib-es/bridge/buildTransaction.d.ts +1 -1
- package/lib-es/bridge/buildTransaction.d.ts.map +1 -1
- package/lib-es/bridge/buildTransaction.integration.test.js +12 -0
- package/lib-es/bridge/buildTransaction.integration.test.js.map +1 -1
- package/lib-es/bridge/buildTransaction.js +9 -5
- package/lib-es/bridge/buildTransaction.js.map +1 -1
- package/lib-es/bridge/buildTransaction.test.js +14 -10
- package/lib-es/bridge/buildTransaction.test.js.map +1 -1
- package/lib-es/bridge/getFeesForTransaction.d.ts.map +1 -1
- package/lib-es/bridge/getFeesForTransaction.js +2 -3
- package/lib-es/bridge/getFeesForTransaction.js.map +1 -1
- package/lib-es/bridge/signOperation.integration.test.js +2 -2
- package/lib-es/bridge/signOperation.integration.test.js.map +1 -1
- package/lib-es/logic/craftTransaction.js +1 -1
- package/lib-es/logic/craftTransaction.js.map +1 -1
- package/lib-es/network/sdk.d.ts +18 -3
- package/lib-es/network/sdk.d.ts.map +1 -1
- package/lib-es/network/sdk.integration.test.js +2 -2
- package/lib-es/network/sdk.integration.test.js.map +1 -1
- package/lib-es/network/sdk.js +61 -48
- package/lib-es/network/sdk.js.map +1 -1
- package/lib-es/network/sdk.test.js +148 -65
- package/lib-es/network/sdk.test.js.map +1 -1
- package/package.json +8 -8
- package/src/api/index.integration.test.ts +2 -2
- package/src/bridge/buildTransaction.integration.test.ts +13 -0
- package/src/bridge/buildTransaction.test.ts +25 -25
- package/src/bridge/buildTransaction.ts +10 -5
- package/src/bridge/getFeesForTransaction.ts +2 -3
- package/src/bridge/signOperation.integration.test.ts +2 -2
- package/src/logic/craftTransaction.ts +1 -1
- package/src/network/sdk.integration.test.ts +2 -2
- package/src/network/sdk.test.ts +186 -77
- package/src/network/sdk.ts +82 -62
- package/tsconfig.json +4 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { findSubAccountById } from "@ledgerhq/coin-framework/account/helpers";
|
|
1
2
|
import type { SuiAccount, Transaction } from "../types";
|
|
2
3
|
import { craftTransaction } from "../logic";
|
|
3
|
-
import { toSuiAsset } from "../network/sdk";
|
|
4
|
+
import { DEFAULT_COIN_TYPE, toSuiAsset } from "../network/sdk";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {Account} account
|
|
@@ -8,13 +9,17 @@ import { toSuiAsset } from "../network/sdk";
|
|
|
8
9
|
*/
|
|
9
10
|
export const buildTransaction = async (
|
|
10
11
|
account: SuiAccount,
|
|
11
|
-
{
|
|
12
|
+
{ amount, mode, recipient, subAccountId }: Transaction,
|
|
12
13
|
) => {
|
|
14
|
+
const { freshAddress } = account;
|
|
15
|
+
const subAccount = findSubAccountById(account, subAccountId ?? "");
|
|
16
|
+
const asset = toSuiAsset(subAccount?.token.contractAddress ?? DEFAULT_COIN_TYPE);
|
|
17
|
+
|
|
13
18
|
return craftTransaction({
|
|
14
|
-
|
|
19
|
+
amount: BigInt(amount.toString()),
|
|
20
|
+
asset,
|
|
15
21
|
recipient,
|
|
22
|
+
sender: freshAddress,
|
|
16
23
|
type: mode,
|
|
17
|
-
amount: BigInt(amount.toString()),
|
|
18
|
-
asset: toSuiAsset(coinType),
|
|
19
24
|
});
|
|
20
25
|
};
|
|
@@ -4,6 +4,7 @@ import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets";
|
|
|
4
4
|
import type { SuiAccount, Transaction } from "../types";
|
|
5
5
|
import { calculateAmount } from "./utils";
|
|
6
6
|
import { estimateFees } from "../logic";
|
|
7
|
+
import { DEFAULT_COIN_TYPE, toSuiAsset } from "../network/sdk";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Fetch the transaction fees for a transaction
|
|
@@ -34,9 +35,7 @@ export default async function getEstimatedFees({
|
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const subAccount = findSubAccountById(account, transaction.subAccountId ?? "");
|
|
37
|
-
const asset = subAccount
|
|
38
|
-
? { type: "token" as const, coinType: subAccount?.token.contractAddress }
|
|
39
|
-
: { type: "native" as const };
|
|
38
|
+
const asset = toSuiAsset(subAccount?.token.contractAddress ?? DEFAULT_COIN_TYPE);
|
|
40
39
|
|
|
41
40
|
const fees = await estimateFees({
|
|
42
41
|
recipient: getAbandonSeedAddress(account.currency.id),
|
|
@@ -2,7 +2,7 @@ import buildSignOperation from "./signOperation";
|
|
|
2
2
|
import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture";
|
|
3
3
|
import { SuiSigner } from "../types";
|
|
4
4
|
import coinConfig from "../config";
|
|
5
|
-
import {
|
|
5
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
6
6
|
|
|
7
7
|
describe("signOperation", () => {
|
|
8
8
|
beforeAll(() => {
|
|
@@ -11,7 +11,7 @@ describe("signOperation", () => {
|
|
|
11
11
|
type: "active",
|
|
12
12
|
},
|
|
13
13
|
node: {
|
|
14
|
-
url:
|
|
14
|
+
url: getEnv("API_SUI_NODE_PROXY"),
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
});
|
|
@@ -25,9 +25,9 @@ export async function craftTransaction({
|
|
|
25
25
|
}
|
|
26
26
|
const unsigned = await suiAPI.createTransaction(sender, {
|
|
27
27
|
amount: BigNumber(amount.toString()),
|
|
28
|
-
recipient,
|
|
29
28
|
coinType,
|
|
30
29
|
mode: type as SuiTransactionMode,
|
|
30
|
+
recipient,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
return { unsigned };
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getBlockInfo,
|
|
13
13
|
getStakes,
|
|
14
14
|
} from "./sdk";
|
|
15
|
-
import {
|
|
15
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
16
16
|
|
|
17
17
|
describe("SUI SDK Integration tests", () => {
|
|
18
18
|
beforeAll(() => {
|
|
@@ -21,7 +21,7 @@ describe("SUI SDK Integration tests", () => {
|
|
|
21
21
|
type: "active",
|
|
22
22
|
},
|
|
23
23
|
node: {
|
|
24
|
-
url:
|
|
24
|
+
url: getEnv("API_SUI_NODE_PROXY"),
|
|
25
25
|
},
|
|
26
26
|
}));
|
|
27
27
|
});
|
package/src/network/sdk.test.ts
CHANGED
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
// Move all jest.mock calls to the very top
|
|
2
|
-
jest.mock("../config", () => ({
|
|
3
|
-
__esModule: true,
|
|
4
|
-
default: {
|
|
5
|
-
getCoinConfig: jest.fn(() => ({ node: { url: "http://test.com" } })),
|
|
6
|
-
setCoinConfig: jest.fn(),
|
|
7
|
-
},
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
jest.mock("../utils", () => ({
|
|
11
|
-
ensureAddressFormat: jest.fn((addr: string) => addr),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
jest.mock("@ledgerhq/live-network/cache", () => ({
|
|
15
|
-
makeLRUCache: jest.fn(() => jest.fn()),
|
|
16
|
-
minutes: jest.fn(() => 60000),
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
jest.mock("@ledgerhq/logs", () => ({
|
|
20
|
-
log: jest.fn(),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
jest.mock("@mysten/sui/client", () => {
|
|
24
|
-
const mockClient = {
|
|
25
|
-
queryTransactionBlocks: jest.fn(),
|
|
26
|
-
getBalance: jest.fn(),
|
|
27
|
-
getLatestCheckpointSequenceNumber: jest.fn(),
|
|
28
|
-
getCheckpoint: jest.fn(),
|
|
29
|
-
dryRunTransactionBlock: jest.fn(),
|
|
30
|
-
executeTransactionBlock: jest.fn(),
|
|
31
|
-
};
|
|
32
|
-
return {
|
|
33
|
-
SuiClient: jest.fn().mockImplementation(() => mockClient),
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
jest.mock("@mysten/sui/transactions", () => ({
|
|
38
|
-
Transaction: jest.fn().mockImplementation(() => ({
|
|
39
|
-
setSender: jest.fn().mockReturnThis(),
|
|
40
|
-
splitCoins: jest.fn().mockReturnValue([{ id: "coin1" }]),
|
|
41
|
-
transferObjects: jest.fn().mockReturnThis(),
|
|
42
|
-
gas: { id: "gas" },
|
|
43
|
-
build: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
|
|
44
|
-
})),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
// Now import after mocks
|
|
48
1
|
import * as sdk from "./sdk";
|
|
49
2
|
import coinConfig from "../config";
|
|
50
3
|
|
|
@@ -215,6 +168,16 @@ const mockTransaction = {
|
|
|
215
168
|
|
|
216
169
|
const mockApi = new SuiClient({ url: "mock" }) as jest.Mocked<SuiClient>;
|
|
217
170
|
|
|
171
|
+
// Helper function to generate mock coins from an array of balances
|
|
172
|
+
const createMockCoins = (balances: string[]): any[] => {
|
|
173
|
+
return balances.map((balance, index) => ({
|
|
174
|
+
coinObjectId: `0xcoin${index + 1}`,
|
|
175
|
+
balance,
|
|
176
|
+
digest: `0xdigest${index + 1}`,
|
|
177
|
+
version: "1",
|
|
178
|
+
}));
|
|
179
|
+
};
|
|
180
|
+
|
|
218
181
|
beforeAll(() => {
|
|
219
182
|
coinConfig.setCoinConfig(() => ({
|
|
220
183
|
status: {
|
|
@@ -320,9 +283,9 @@ describe("SDK Functions", () => {
|
|
|
320
283
|
});
|
|
321
284
|
|
|
322
285
|
test("getOperationDate should return correct date", () => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
286
|
+
const date = sdk.getOperationDate(mockTransaction as SuiTransactionBlockResponse);
|
|
287
|
+
expect(date).toBeDefined();
|
|
288
|
+
expect(date).toBeInstanceOf(Date);
|
|
326
289
|
});
|
|
327
290
|
|
|
328
291
|
test("getOperationCoinType should extract token coin type", () => {
|
|
@@ -534,33 +497,6 @@ describe("SDK Functions", () => {
|
|
|
534
497
|
expect(info).toHaveProperty("fees");
|
|
535
498
|
});
|
|
536
499
|
|
|
537
|
-
test("getCoinObjectIds should return array of object IDs for token transactions", async () => {
|
|
538
|
-
const address = "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0";
|
|
539
|
-
const transaction = {
|
|
540
|
-
mode: "token.send" as const,
|
|
541
|
-
coinType: "0x123::test::TOKEN",
|
|
542
|
-
amount: new BigNumber(100),
|
|
543
|
-
recipient: "0x33444cf803c690db96527cec67e3c9ab512596f4ba2d4eace43f0b4f716e0164",
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const coinObjectIds = await sdk.getCoinObjectIds(address, transaction);
|
|
547
|
-
expect(Array.isArray(coinObjectIds)).toBe(true);
|
|
548
|
-
expect(coinObjectIds).toContain("0xtest_coin_object_id");
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("getCoinObjectIds should return null for SUI transactions", async () => {
|
|
552
|
-
const address = "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0";
|
|
553
|
-
const transaction = {
|
|
554
|
-
mode: "send" as const,
|
|
555
|
-
coinType: sdk.DEFAULT_COIN_TYPE,
|
|
556
|
-
amount: new BigNumber(100),
|
|
557
|
-
recipient: "0x33444cf803c690db96527cec67e3c9ab512596f4ba2d4eace43f0b4f716e0164",
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const coinObjectIds = await sdk.getCoinObjectIds(address, transaction);
|
|
561
|
-
expect(coinObjectIds).toBeNull();
|
|
562
|
-
});
|
|
563
|
-
|
|
564
500
|
test("createTransaction should build a transaction", async () => {
|
|
565
501
|
const address = "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0";
|
|
566
502
|
const transaction = {
|
|
@@ -1531,3 +1467,176 @@ describe("filterOperations", () => {
|
|
|
1531
1467
|
});
|
|
1532
1468
|
});
|
|
1533
1469
|
});
|
|
1470
|
+
|
|
1471
|
+
describe("getCoinsForAmount", () => {
|
|
1472
|
+
const mockAddress = "0x33444cf803c690db96527cec67e3c9ab512596f4ba2d4eace43f0b4f716e0164";
|
|
1473
|
+
const mockCoinType = "0x2::sui::SUI";
|
|
1474
|
+
|
|
1475
|
+
beforeEach(() => {
|
|
1476
|
+
mockApi.getCoins.mockReset();
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
describe("basic functionality", () => {
|
|
1480
|
+
test("handles single coin scenarios", async () => {
|
|
1481
|
+
const sufficientCoins = createMockCoins(["1000"]);
|
|
1482
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: sufficientCoins, hasNextPage: false });
|
|
1483
|
+
|
|
1484
|
+
let result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1485
|
+
expect(result).toHaveLength(1);
|
|
1486
|
+
expect(result[0].balance).toBe("1000");
|
|
1487
|
+
|
|
1488
|
+
const insufficientCoins = createMockCoins(["500"]);
|
|
1489
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: insufficientCoins, hasNextPage: false });
|
|
1490
|
+
|
|
1491
|
+
result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1492
|
+
expect(result).toHaveLength(1);
|
|
1493
|
+
expect(result[0].balance).toBe("500");
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
test("selects minimum coins needed", async () => {
|
|
1497
|
+
const exactMatchCoins = createMockCoins(["600", "400", "300"]);
|
|
1498
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: exactMatchCoins, hasNextPage: false });
|
|
1499
|
+
|
|
1500
|
+
let result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1501
|
+
expect(result).toHaveLength(2);
|
|
1502
|
+
expect(result[0].balance).toBe("600");
|
|
1503
|
+
expect(result[1].balance).toBe("400");
|
|
1504
|
+
|
|
1505
|
+
const exceedCoins = createMockCoins(["800", "400", "200"]);
|
|
1506
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: exceedCoins, hasNextPage: false });
|
|
1507
|
+
|
|
1508
|
+
result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1509
|
+
expect(result).toHaveLength(2);
|
|
1510
|
+
expect(result[0].balance).toBe("800");
|
|
1511
|
+
expect(result[1].balance).toBe("400");
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("handles edge cases", async () => {
|
|
1515
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: [], hasNextPage: false });
|
|
1516
|
+
|
|
1517
|
+
let result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1518
|
+
expect(result).toHaveLength(0);
|
|
1519
|
+
|
|
1520
|
+
const coins = createMockCoins(["1000"]);
|
|
1521
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: coins, hasNextPage: false });
|
|
1522
|
+
|
|
1523
|
+
result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 0);
|
|
1524
|
+
expect(result).toHaveLength(0);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
describe("sorting and filtering", () => {
|
|
1529
|
+
test("filters zero balance coins", async () => {
|
|
1530
|
+
const mockCoins = createMockCoins(["1000", "500"]);
|
|
1531
|
+
mockCoins.splice(1, 0, createMockCoins(["0"])[0]);
|
|
1532
|
+
mockCoins.push({ coinObjectId: "0xcoin4", balance: "0", digest: "0xdigest4", version: "1" });
|
|
1533
|
+
|
|
1534
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: mockCoins, hasNextPage: false });
|
|
1535
|
+
|
|
1536
|
+
const result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1537
|
+
|
|
1538
|
+
expect(result).toHaveLength(1);
|
|
1539
|
+
expect(result[0].balance).toBe("1000");
|
|
1540
|
+
expect(result.every(coin => parseInt(coin.balance) > 0)).toBe(true);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
test("sorts and optimizes coin selection", async () => {
|
|
1544
|
+
const unsortedCoins = createMockCoins(["100", "800", "300", "500"]);
|
|
1545
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: unsortedCoins, hasNextPage: false });
|
|
1546
|
+
|
|
1547
|
+
let result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1548
|
+
expect(result).toHaveLength(2);
|
|
1549
|
+
expect(result[0].balance).toBe("800");
|
|
1550
|
+
expect(result[1].balance).toBe("500");
|
|
1551
|
+
|
|
1552
|
+
const mixedCoins = createMockCoins(["200", "800", "400"]);
|
|
1553
|
+
mixedCoins.unshift(createMockCoins(["0"])[0]);
|
|
1554
|
+
mixedCoins.splice(2, 0, createMockCoins(["0"])[0]);
|
|
1555
|
+
|
|
1556
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: mixedCoins, hasNextPage: false });
|
|
1557
|
+
|
|
1558
|
+
result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1559
|
+
expect(result).toHaveLength(2);
|
|
1560
|
+
expect(result[0].balance).toBe("800");
|
|
1561
|
+
expect(result[1].balance).toBe("400");
|
|
1562
|
+
expect(result.every(coin => parseInt(coin.balance) > 0)).toBe(true);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test("handles all zero balance coins", async () => {
|
|
1566
|
+
const mockCoins = createMockCoins(["0", "0", "0"]);
|
|
1567
|
+
mockApi.getCoins.mockResolvedValueOnce({ data: mockCoins, hasNextPage: false });
|
|
1568
|
+
|
|
1569
|
+
const result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1570
|
+
|
|
1571
|
+
expect(result).toHaveLength(0);
|
|
1572
|
+
expect(result).toEqual([]);
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
describe("pagination", () => {
|
|
1577
|
+
test("handles single page scenarios", async () => {
|
|
1578
|
+
const mockCoins = createMockCoins(["800", "400", "300"]);
|
|
1579
|
+
mockApi.getCoins.mockResolvedValueOnce({
|
|
1580
|
+
data: mockCoins,
|
|
1581
|
+
hasNextPage: true,
|
|
1582
|
+
nextCursor: "cursor1",
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
const result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1586
|
+
|
|
1587
|
+
expect(result).toHaveLength(2);
|
|
1588
|
+
expect(result[0].balance).toBe("800");
|
|
1589
|
+
expect(result[1].balance).toBe("400");
|
|
1590
|
+
expect(mockApi.getCoins).toHaveBeenCalledTimes(1);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
test("handles multi-page scenarios", async () => {
|
|
1594
|
+
const firstPageCoins = createMockCoins(["300", "200"]);
|
|
1595
|
+
const secondPageCoins = createMockCoins(["600", "400", "100"]);
|
|
1596
|
+
|
|
1597
|
+
mockApi.getCoins
|
|
1598
|
+
.mockResolvedValueOnce({
|
|
1599
|
+
data: firstPageCoins,
|
|
1600
|
+
hasNextPage: true,
|
|
1601
|
+
nextCursor: "cursor1",
|
|
1602
|
+
})
|
|
1603
|
+
.mockResolvedValueOnce({
|
|
1604
|
+
data: secondPageCoins,
|
|
1605
|
+
hasNextPage: false,
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
const result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1609
|
+
|
|
1610
|
+
expect(result).toHaveLength(3);
|
|
1611
|
+
expect(result[0].balance).toBe("300");
|
|
1612
|
+
expect(result[1].balance).toBe("200");
|
|
1613
|
+
expect(result[2].balance).toBe("600");
|
|
1614
|
+
expect(mockApi.getCoins).toHaveBeenCalledTimes(2);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
test("handles insufficient funds across pages", async () => {
|
|
1618
|
+
const firstPageCoins = createMockCoins(["300", "200"]);
|
|
1619
|
+
const secondPageCoins = createMockCoins(["200", "100"]);
|
|
1620
|
+
|
|
1621
|
+
mockApi.getCoins
|
|
1622
|
+
.mockResolvedValueOnce({
|
|
1623
|
+
data: firstPageCoins,
|
|
1624
|
+
hasNextPage: true,
|
|
1625
|
+
nextCursor: "cursor1",
|
|
1626
|
+
})
|
|
1627
|
+
.mockResolvedValueOnce({
|
|
1628
|
+
data: secondPageCoins,
|
|
1629
|
+
hasNextPage: false,
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
const result = await sdk.getCoinsForAmount(mockApi, mockAddress, mockCoinType, 1000);
|
|
1633
|
+
|
|
1634
|
+
expect(result).toHaveLength(4);
|
|
1635
|
+
expect(result[0].balance).toBe("300");
|
|
1636
|
+
expect(result[1].balance).toBe("200");
|
|
1637
|
+
expect(result[2].balance).toBe("200");
|
|
1638
|
+
expect(result[3].balance).toBe("100");
|
|
1639
|
+
expect(mockApi.getCoins).toHaveBeenCalledTimes(2);
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
});
|
package/src/network/sdk.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BalanceChange,
|
|
2
3
|
Checkpoint,
|
|
3
4
|
ExecuteTransactionBlockParams,
|
|
4
5
|
PaginatedTransactionResponse,
|
|
6
|
+
QueryTransactionBlocksParams,
|
|
5
7
|
SuiCallArg,
|
|
6
8
|
SuiClient,
|
|
7
|
-
SuiTransactionBlockResponse,
|
|
8
|
-
TransactionBlockData,
|
|
9
9
|
SuiHTTPTransport,
|
|
10
|
-
|
|
11
|
-
QueryTransactionBlocksParams,
|
|
12
|
-
BalanceChange,
|
|
10
|
+
SuiTransactionBlockResponse,
|
|
13
11
|
SuiTransactionBlockResponseOptions,
|
|
14
12
|
DelegatedStake,
|
|
15
13
|
StakeObject,
|
|
14
|
+
TransactionBlockData,
|
|
15
|
+
TransactionEffects,
|
|
16
16
|
} from "@mysten/sui/client";
|
|
17
17
|
import { Transaction } from "@mysten/sui/transactions";
|
|
18
18
|
import { BigNumber } from "bignumber.js";
|
|
@@ -481,75 +481,79 @@ const getTotalGasUsed = (effects?: TransactionEffects | null): bigint => {
|
|
|
481
481
|
);
|
|
482
482
|
};
|
|
483
483
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Get coins for a given address and coin type, stopping when we have enough to cover the amount.
|
|
486
|
+
* Returns the minimum coins needed to cover the required amount.
|
|
487
|
+
*/
|
|
488
|
+
export const getCoinsForAmount = async (
|
|
489
|
+
api: SuiClient,
|
|
490
|
+
address: string,
|
|
491
|
+
coinType: string,
|
|
492
|
+
requiredAmount: number,
|
|
493
|
+
) => {
|
|
494
|
+
const coins = [];
|
|
495
|
+
let cursor = null;
|
|
496
|
+
let hasNextPage = true;
|
|
497
|
+
let totalBalance = 0;
|
|
498
|
+
|
|
499
|
+
while (hasNextPage && totalBalance < requiredAmount) {
|
|
500
|
+
const response = await api.getCoins({
|
|
501
|
+
owner: address,
|
|
502
|
+
coinType,
|
|
503
|
+
cursor,
|
|
504
|
+
});
|
|
488
505
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const txb = await tx.build({ client: api });
|
|
502
|
-
const dryRunTxResponse = await api.dryRunTransactionBlock({ transactionBlock: txb });
|
|
503
|
-
const fees = getTotalGasUsed(dryRunTxResponse.effects);
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
gasBudget: dryRunTxResponse.input.gasData.budget,
|
|
507
|
-
totalGasUsed: fees,
|
|
508
|
-
fees,
|
|
509
|
-
};
|
|
510
|
-
} catch (error) {
|
|
511
|
-
console.warn("Fee estimation failed:", error);
|
|
512
|
-
// If dry run fails return a reasonable default gas budget as fallback
|
|
513
|
-
return {
|
|
514
|
-
gasBudget: Array.isArray(coinObjects)
|
|
515
|
-
? FALLBACK_GAS_BUDGET.TOKEN_TRANSFER
|
|
516
|
-
: FALLBACK_GAS_BUDGET.SUI_TRANSFER,
|
|
517
|
-
totalGasUsed: BigInt(1000000),
|
|
518
|
-
fees: BigInt(1000000),
|
|
519
|
-
};
|
|
506
|
+
// Filter out zero-balance coins and sort by balance (largest first)
|
|
507
|
+
const validCoins = response.data
|
|
508
|
+
.filter(coin => parseInt(coin.balance) > 0)
|
|
509
|
+
.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
|
|
510
|
+
|
|
511
|
+
let currentBalance = totalBalance;
|
|
512
|
+
let i = 0;
|
|
513
|
+
while (i < validCoins.length && currentBalance < requiredAmount) {
|
|
514
|
+
const coin = validCoins[i];
|
|
515
|
+
coins.push(coin);
|
|
516
|
+
currentBalance += parseInt(coin.balance);
|
|
517
|
+
i++;
|
|
520
518
|
}
|
|
521
|
-
|
|
519
|
+
totalBalance = currentBalance;
|
|
522
520
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
) =>
|
|
527
|
-
withApi(async api => {
|
|
528
|
-
const coinObjectId = null;
|
|
521
|
+
cursor = response.nextCursor;
|
|
522
|
+
hasNextPage = response.hasNextPage && totalBalance < requiredAmount;
|
|
523
|
+
}
|
|
529
524
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
owner: address,
|
|
533
|
-
coinType: transaction.coinType,
|
|
534
|
-
});
|
|
535
|
-
return tokenInfo.data.map(coin => coin.coinObjectId);
|
|
536
|
-
}
|
|
537
|
-
return coinObjectId;
|
|
538
|
-
});
|
|
525
|
+
return coins;
|
|
526
|
+
};
|
|
539
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Creates a Sui transaction block for transferring coins.
|
|
530
|
+
*
|
|
531
|
+
* @param address - The sender's address
|
|
532
|
+
* @param transaction - The transaction details including recipient, amount, and coin type
|
|
533
|
+
* @returns Promise<TransactionBlock> - A built transaction block ready for execution
|
|
534
|
+
*
|
|
535
|
+
*/
|
|
540
536
|
export const createTransaction = async (address: string, transaction: CreateExtrinsicArg) =>
|
|
541
537
|
withApi(async api => {
|
|
542
538
|
const tx = new Transaction();
|
|
543
539
|
tx.setSender(ensureAddressFormat(address));
|
|
544
540
|
|
|
545
|
-
|
|
541
|
+
if (transaction.coinType !== DEFAULT_COIN_TYPE) {
|
|
542
|
+
const requiredAmount = transaction.amount.toNumber();
|
|
543
|
+
|
|
544
|
+
const coins = await getCoinsForAmount(api, address, transaction.coinType, requiredAmount);
|
|
546
545
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (coins.length > 1) {
|
|
550
|
-
tx.mergeCoins(coins[0], coins.slice(1));
|
|
546
|
+
if (coins.length === 0) {
|
|
547
|
+
throw new Error(`No coins found for type ${transaction.coinType}`);
|
|
551
548
|
}
|
|
552
|
-
|
|
549
|
+
|
|
550
|
+
const coinObjects = coins.map(coin => tx.object(coin.coinObjectId));
|
|
551
|
+
|
|
552
|
+
if (coinObjects.length > 1) {
|
|
553
|
+
tx.mergeCoins(coinObjects[0], coinObjects.slice(1));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const [coin] = tx.splitCoins(coinObjects[0], [transaction.amount.toNumber()]);
|
|
553
557
|
tx.transferObjects([coin], transaction.recipient);
|
|
554
558
|
} else {
|
|
555
559
|
const [coin] = tx.splitCoins(tx.gas, [transaction.amount.toNumber()]);
|
|
@@ -559,6 +563,22 @@ export const createTransaction = async (address: string, transaction: CreateExtr
|
|
|
559
563
|
return tx.build({ client: api });
|
|
560
564
|
});
|
|
561
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Performs a dry run of a transaction to estimate gas costs and fees
|
|
568
|
+
*/
|
|
569
|
+
export const paymentInfo = async (sender: string, fakeTransaction: TransactionType) =>
|
|
570
|
+
withApi(async api => {
|
|
571
|
+
const txb = await createTransaction(sender, fakeTransaction);
|
|
572
|
+
const dryRunTxResponse = await api.dryRunTransactionBlock({ transactionBlock: txb });
|
|
573
|
+
const fees = getTotalGasUsed(dryRunTxResponse.effects);
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
gasBudget: dryRunTxResponse.input.gasData.budget,
|
|
577
|
+
totalGasUsed: fees,
|
|
578
|
+
fees,
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
|
|
562
582
|
export const executeTransactionBlock = async (params: ExecuteTransactionBlockParams) =>
|
|
563
583
|
withApi(async api => {
|
|
564
584
|
return api.executeTransactionBlock(params);
|
package/tsconfig.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"rootDir": "./src",
|
|
9
9
|
"exactOptionalPropertyTypes": true,
|
|
10
10
|
"module": "ESNext",
|
|
11
|
-
"moduleResolution": "bundler"
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"typeRoots": ["./node_modules/@types", "index.d.ts"]
|
|
12
13
|
},
|
|
13
|
-
"include": ["src/**/*"]
|
|
14
|
-
}
|
|
14
|
+
"include": ["src/**/*", "index.d.ts"]
|
|
15
|
+
}
|