Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
kien-ngo committed Nov 1, 2024
1 parent f40d247 commit 017315e
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-trees-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Optimize ERC20 transferBatch
77 changes: 67 additions & 10 deletions packages/thirdweb/src/extensions/erc20/write/transferBatch.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
import { TEST_CLIENT } from "~test/test-clients.js";
Expand All @@ -8,19 +8,23 @@ import {
TEST_ACCOUNT_C,
TEST_ACCOUNT_D,
} from "~test/test-wallets.js";
import { getContract } from "../../../contract/contract.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { deployERC20Contract } from "../../../extensions/prebuilts/deploy-erc20.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { balanceOf } from "../__generated__/IERC20/read/balanceOf.js";
import { mintTo } from "./mintTo.js";
import { transferBatch } from "./transferBatch.js";
import { optimizeTransferContent, transferBatch } from "./transferBatch.js";

const chain = ANVIL_CHAIN;
const client = TEST_CLIENT;
const account = TEST_ACCOUNT_A;
let contract: ThirdwebContract;

describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
it("should transfer tokens to multiple recipients", async () => {
beforeAll(async () => {
const address = await deployERC20Contract({
type: "TokenERC20",
account,
Expand All @@ -31,15 +35,16 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
contractURI: TEST_CONTRACT_URI,
},
});
const contract = getContract({
contract = getContract({
address,
chain,
client,
});

// Mint 100 tokens
}, 60_000_000);
it("should transfer tokens to multiple recipients", async () => {
// Mint 200 tokens
await sendAndConfirmTransaction({
transaction: mintTo({ contract, to: account.address, amount: 100 }),
transaction: mintTo({ contract, to: account.address, amount: 200 }),
account,
});

Expand All @@ -61,6 +66,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
to: TEST_ACCOUNT_D.address,
amount: 25,
},
{
to: TEST_ACCOUNT_B.address.toLowerCase(),
amount: 25,
},
{
to: TEST_ACCOUNT_B.address,
amountWei: 25n * 10n ** 18n,
},
],
}),
});
Expand All @@ -73,9 +86,53 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc20: transferBatch", () => {
balanceOf({ contract, address: TEST_ACCOUNT_D.address }),
]);

expect(balanceA).toBe(25n * 10n ** 18n);
expect(balanceB).toBe(25n * 10n ** 18n);
expect(balanceA).toBe(75n * 10n ** 18n);
expect(balanceB).toBe(75n * 10n ** 18n);
expect(balanceC).toBe(25n * 10n ** 18n);
expect(balanceD).toBe(25n * 10n ** 18n);
});

it("should optimize the transfer content", async () => {
const content = await optimizeTransferContent({
contract,
batch: [
{
to: TEST_ACCOUNT_B.address,
amount: 25,
},
{
to: TEST_ACCOUNT_C.address,
amount: 25,
},
{
to: TEST_ACCOUNT_D.address,
amount: 25,
},
{
// Should work
to: TEST_ACCOUNT_B.address.toLowerCase(),
amount: 25,
},
{
to: TEST_ACCOUNT_B.address,
amountWei: 25n * 10n ** 18n,
},
],
});

expect(content).toStrictEqual([
{
to: TEST_ACCOUNT_B.address,
amountWei: 75n * 10n ** 18n,
},
{
to: TEST_ACCOUNT_C.address,
amountWei: 25n * 10n ** 18n,
},
{
to: TEST_ACCOUNT_D.address,
amountWei: 25n * 10n ** 18n,
},
]);
});
});
99 changes: 74 additions & 25 deletions packages/thirdweb/src/extensions/erc20/write/transferBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,83 @@ export function transferBatch(
return multicall({
contract: options.contract,
asyncParams: async () => {
const content = await optimizeTransferContent(options);
return {
data: await Promise.all(
options.batch.map(async (transfer) => {
let amount: bigint;
if ("amount" in transfer) {
// if we need to parse the amount from ether to gwei then we pull in the decimals extension
const { decimals } = await import("../read/decimals.js");
// it's OK to call this multiple times because the call is cached
// if this fails we fall back to `18` decimals
const d = await decimals(options).catch(() => 18);
// turn ether into gwei
amount = toUnits(transfer.amount.toString(), d);
} else {
amount = transfer.amountWei;
}
return encodeTransfer({
to: transfer.to,
value: amount,
overrides: {
erc20Value: {
amountWei: amount,
tokenAddress: options.contract.address,
},
data: content.map((item) => {
return encodeTransfer({
to: item.to,
value: item.amountWei,
overrides: {
erc20Value: {
amountWei: item.amountWei,
tokenAddress: options.contract.address,
},
});
}),
),
},
});
}),
};
},
});
}

/**
* Records with the same recipient (`to`) can be packed into one transaction
* For example, the data below:
* ```ts
* [
* {
* to: "wallet-a",
* amount: 1,
* },
* {
* to: "wallet-A",
* amountWei: 1000000000000000000n,
* },
* ]
* ```
*
* can be packed to:
* ```ts
* [
* {
* to: "wallet-a",
* amountWei: 2000000000000000000n,
* },
* ]
* ```
* @internal
*/
export async function optimizeTransferContent(
options: BaseTransactionOptions<TransferBatchParams>,
): Promise<Array<{ to: string; amountWei: bigint }>> {
const optimizedData: Array<{ to: string; amountWei: bigint }> = [];
for (const item of options.batch) {
let amountWei = 0n;
if ("amount" in item) {
// if we need to parse the amount from ether to gwei then we pull in the decimals extension
const { decimals } = await import("../read/decimals.js");
// it's OK to call this multiple times because the call is cached
// if this fails we fall back to `18` decimals
const d = await decimals(options).catch(() => 18);
// turn ether into gwei
amountWei = toUnits(item.amount.toString(), d);
} else {
amountWei = item.amountWei;
}
const matchingIndex = optimizedData.findIndex(
(o) => o.to.toLowerCase() === item.to.toLowerCase(),
);
if (matchingIndex !== -1) {
optimizedData[matchingIndex] = {
to: item.to,
amountWei: (optimizedData[matchingIndex]?.amountWei || 0n) + amountWei,
};
} else {
optimizedData.push({
to: item.to,
amountWei,
});
}
}
return optimizedData;
}

0 comments on commit 017315e

Please sign in to comment.