Skip to content

Commit f1c98a1

Browse files
committed
update
1 parent 322e449 commit f1c98a1

File tree

3 files changed

+186
-35
lines changed

3 files changed

+186
-35
lines changed

.changeset/olive-trees-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Optimize ERC20 transferBatch

packages/thirdweb/src/extensions/erc20/write/transferBatch.test.ts

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from "vitest";
1+
import { beforeAll, describe, expect, it } from "vitest";
22
import { ANVIL_CHAIN } from "~test/chains.js";
33
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
44
import { TEST_CLIENT } from "~test/test-clients.js";
@@ -8,19 +8,23 @@ import {
88
TEST_ACCOUNT_C,
99
TEST_ACCOUNT_D,
1010
} from "~test/test-wallets.js";
11-
import { getContract } from "../../../contract/contract.js";
11+
import {
12+
type ThirdwebContract,
13+
getContract,
14+
} from "../../../contract/contract.js";
1215
import { deployERC20Contract } from "../../../extensions/prebuilts/deploy-erc20.js";
1316
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
1417
import { balanceOf } from "../__generated__/IERC20/read/balanceOf.js";
1518
import { mintTo } from "./mintTo.js";
16-
import { transferBatch } from "./transferBatch.js";
19+
import { optimizeTransferContent, transferBatch } from "./transferBatch.js";
1720

1821
const chain = ANVIL_CHAIN;
1922
const client = TEST_CLIENT;
2023
const account = TEST_ACCOUNT_A;
24+
let contract: ThirdwebContract;
2125

2226
describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
23-
it("should transfer tokens to multiple recipients", async () => {
27+
beforeAll(async () => {
2428
const address = await deployERC20Contract({
2529
type: "TokenERC20",
2630
account,
@@ -31,15 +35,16 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
3135
contractURI: TEST_CONTRACT_URI,
3236
},
3337
});
34-
const contract = getContract({
38+
contract = getContract({
3539
address,
3640
chain,
3741
client,
3842
});
39-
40-
// Mint 100 tokens
43+
}, 60_000_000);
44+
it("should transfer tokens to multiple recipients", async () => {
45+
// Mint 200 tokens
4146
await sendAndConfirmTransaction({
42-
transaction: mintTo({ contract, to: account.address, amount: 100 }),
47+
transaction: mintTo({ contract, to: account.address, amount: 200 }),
4348
account,
4449
});
4550

@@ -61,6 +66,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
6166
to: TEST_ACCOUNT_D.address,
6267
amount: 25,
6368
},
69+
{
70+
to: TEST_ACCOUNT_B.address.toLowerCase(),
71+
amount: 25,
72+
},
73+
{
74+
to: TEST_ACCOUNT_B.address,
75+
amountWei: 25n * 10n ** 18n,
76+
},
6477
],
6578
}),
6679
});
@@ -73,9 +86,88 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
7386
balanceOf({ contract, address: TEST_ACCOUNT_D.address }),
7487
]);
7588

76-
expect(balanceA).toBe(25n * 10n ** 18n);
77-
expect(balanceB).toBe(25n * 10n ** 18n);
89+
expect(balanceA).toBe(75n * 10n ** 18n);
90+
expect(balanceB).toBe(75n * 10n ** 18n);
7891
expect(balanceC).toBe(25n * 10n ** 18n);
7992
expect(balanceD).toBe(25n * 10n ** 18n);
8093
});
94+
95+
it("should optimize the transfer content", async () => {
96+
const content = await optimizeTransferContent({
97+
contract,
98+
batch: [
99+
{
100+
to: TEST_ACCOUNT_B.address,
101+
amount: 25,
102+
},
103+
{
104+
to: TEST_ACCOUNT_C.address,
105+
amount: 25,
106+
},
107+
{
108+
to: TEST_ACCOUNT_D.address,
109+
amount: 25,
110+
},
111+
{
112+
// Should work
113+
to: TEST_ACCOUNT_B.address.toLowerCase(),
114+
amount: 25,
115+
},
116+
{
117+
to: TEST_ACCOUNT_B.address,
118+
amountWei: 25n * 10n ** 18n,
119+
},
120+
],
121+
});
122+
123+
expect(content).toStrictEqual([
124+
{
125+
to: TEST_ACCOUNT_B.address,
126+
amountWei: 75n * 10n ** 18n,
127+
},
128+
{
129+
to: TEST_ACCOUNT_C.address,
130+
amountWei: 25n * 10n ** 18n,
131+
},
132+
{
133+
to: TEST_ACCOUNT_D.address,
134+
amountWei: 25n * 10n ** 18n,
135+
},
136+
]);
137+
});
138+
139+
it("an already-optimized content should not be changed", async () => {
140+
const content = await optimizeTransferContent({
141+
contract,
142+
batch: [
143+
{
144+
to: TEST_ACCOUNT_B.address,
145+
amountWei: 25n * 10n ** 18n,
146+
},
147+
{
148+
to: TEST_ACCOUNT_C.address,
149+
amount: 25,
150+
},
151+
{
152+
to: TEST_ACCOUNT_D.address,
153+
amount: 25,
154+
},
155+
],
156+
});
157+
158+
expect(content).toStrictEqual([
159+
{
160+
to: TEST_ACCOUNT_B.address,
161+
amountWei: 25n * 10n ** 18n,
162+
},
163+
{
164+
to: TEST_ACCOUNT_C.address,
165+
amountWei: 25n * 10n ** 18n,
166+
},
167+
{
168+
to: TEST_ACCOUNT_D.address,
169+
amountWei: 25n * 10n ** 18n,
170+
},
171+
]);
172+
});
81173
});

packages/thirdweb/src/extensions/erc20/write/transferBatch.ts

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,88 @@ export function transferBatch(
5353
return multicall({
5454
contract: options.contract,
5555
asyncParams: async () => {
56+
const content = await optimizeTransferContent(options);
5657
return {
57-
data: await Promise.all(
58-
options.batch.map(async (transfer) => {
59-
let amount: bigint;
60-
if ("amount" in transfer) {
61-
// if we need to parse the amount from ether to gwei then we pull in the decimals extension
62-
const { decimals } = await import("../read/decimals.js");
63-
// it's OK to call this multiple times because the call is cached
64-
// if this fails we fall back to `18` decimals
65-
const d = await decimals(options).catch(() => 18);
66-
// turn ether into gwei
67-
amount = toUnits(transfer.amount.toString(), d);
68-
} else {
69-
amount = transfer.amountWei;
70-
}
71-
return encodeTransfer({
72-
to: transfer.to,
73-
value: amount,
74-
overrides: {
75-
erc20Value: {
76-
amountWei: amount,
77-
tokenAddress: options.contract.address,
78-
},
58+
data: content.map((item) => {
59+
return encodeTransfer({
60+
to: item.to,
61+
value: item.amountWei,
62+
overrides: {
63+
erc20Value: {
64+
amountWei: item.amountWei,
65+
tokenAddress: options.contract.address,
7966
},
80-
});
81-
}),
82-
),
67+
},
68+
});
69+
}),
8370
};
8471
},
8572
});
8673
}
74+
75+
/**
76+
* Records with the same recipient (`to`) can be packed into one transaction
77+
* For example, the data below:
78+
* ```ts
79+
* [
80+
* {
81+
* to: "wallet-a",
82+
* amount: 1,
83+
* },
84+
* {
85+
* to: "wallet-A",
86+
* amountWei: 1000000000000000000n,
87+
* },
88+
* ]
89+
* ```
90+
*
91+
* can be packed to:
92+
* ```ts
93+
* [
94+
* {
95+
* to: "wallet-a",
96+
* amountWei: 2000000000000000000n,
97+
* },
98+
* ]
99+
* ```
100+
* @internal
101+
*/
102+
export async function optimizeTransferContent(
103+
options: BaseTransactionOptions<TransferBatchParams>,
104+
): Promise<Array<{ to: string; amountWei: bigint }>> {
105+
const groupedRecords = await options.batch.reduce(
106+
async (accPromise, record) => {
107+
const acc = await accPromise;
108+
let amountInWei: bigint;
109+
if ("amount" in record) {
110+
// it's OK to call this multiple times because the call is cached
111+
const { decimals } = await import("../read/decimals.js");
112+
// if this fails we fall back to `18` decimals
113+
const d = await decimals(options).catch(() => undefined);
114+
if (d === undefined) {
115+
throw new Error(
116+
`Failed to get the decimals for contract: ${options.contract.address}`,
117+
);
118+
}
119+
amountInWei = toUnits(record.amount.toString(), d);
120+
} else {
121+
amountInWei = record.amountWei;
122+
}
123+
const existingRecord = acc.find(
124+
(r) => r.to.toLowerCase() === record.to.toLowerCase(),
125+
);
126+
if (existingRecord) {
127+
existingRecord.amountWei = existingRecord.amountWei + amountInWei;
128+
} else {
129+
acc.push({
130+
to: record.to,
131+
amountWei: amountInWei,
132+
});
133+
}
134+
135+
return acc;
136+
},
137+
Promise.resolve([] as { to: string; amountWei: bigint }[]),
138+
);
139+
return groupedRecords;
140+
}

0 commit comments

Comments
 (0)