@ledgerhq/coin-sui 0.10.0 → 0.10.1-nightly.1
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/.eslintrc.js +1 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +27 -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/estimateMaxSpendable.js +1 -1
- package/lib/bridge/estimateMaxSpendable.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 +68 -55
- 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/estimateMaxSpendable.js +1 -1
- package/lib-es/bridge/estimateMaxSpendable.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 +65 -52
- 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 +10 -10
- 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/estimateMaxSpendable.ts +1 -1
- 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 +86 -66
- package/tsconfig.json +4 -3
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";
|
|
@@ -232,7 +232,7 @@ export function transactionToOperation(
|
|
|
232
232
|
coinType,
|
|
233
233
|
},
|
|
234
234
|
fee: getOperationFee(transaction),
|
|
235
|
-
hasFailed: transaction.effects?.status.status
|
|
235
|
+
hasFailed: transaction.effects?.status.status !== "success",
|
|
236
236
|
hash,
|
|
237
237
|
recipients: getOperationRecipients(transaction.transaction?.data),
|
|
238
238
|
senders: getOperationSenders(transaction.transaction?.data),
|
|
@@ -278,7 +278,7 @@ export function toBlockInfo(checkpoint: Checkpoint): BlockInfo {
|
|
|
278
278
|
time: new Date(parseInt(checkpoint.timestampMs)),
|
|
279
279
|
};
|
|
280
280
|
|
|
281
|
-
if (typeof checkpoint.previousDigest
|
|
281
|
+
if (typeof checkpoint.previousDigest === "string")
|
|
282
282
|
return {
|
|
283
283
|
...info,
|
|
284
284
|
parent: {
|
|
@@ -303,7 +303,7 @@ export function toBlockInfo(checkpoint: Checkpoint): BlockInfo {
|
|
|
303
303
|
export function toBlockTransaction(transaction: SuiTransactionBlockResponse): BlockTransaction {
|
|
304
304
|
return {
|
|
305
305
|
hash: transaction.digest,
|
|
306
|
-
failed: transaction.effects?.status.status
|
|
306
|
+
failed: transaction.effects?.status.status !== "success",
|
|
307
307
|
operations: transaction.balanceChanges?.flatMap(toBlockOperation) || [],
|
|
308
308
|
fees: BigInt(getOperationFee(transaction).toString()),
|
|
309
309
|
feesPayer: transaction.transaction?.data.sender || "",
|
|
@@ -316,7 +316,7 @@ export function toBlockTransaction(transaction: SuiTransactionBlockResponse): Bl
|
|
|
316
316
|
* @param change balance change
|
|
317
317
|
*/
|
|
318
318
|
export function toBlockOperation(change: BalanceChange): BlockOperation[] {
|
|
319
|
-
if (typeof change.owner
|
|
319
|
+
if (typeof change.owner === "string" || !("AddressOwner" in change.owner)) return [];
|
|
320
320
|
return [
|
|
321
321
|
{
|
|
322
322
|
type: "transfer",
|
|
@@ -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
|
+
}
|