diff --git a/.changeset/blue-eels-visit.md b/.changeset/blue-eels-visit.md new file mode 100644 index 00000000..2ccb7e2f --- /dev/null +++ b/.changeset/blue-eels-visit.md @@ -0,0 +1,5 @@ +--- +'@soundxyz/sdk': patch +--- + +Add calldata compression to mint function diff --git a/examples/nextjs/src/app/v2/page.tsx b/examples/nextjs/src/app/v2/page.tsx index 0093d86a..ed63039c 100644 --- a/examples/nextjs/src/app/v2/page.tsx +++ b/examples/nextjs/src/app/v2/page.tsx @@ -91,8 +91,8 @@ function EditionSchedule({ schedule }: { schedule: SuperMinterSchedule }) { (schedule.tier === 0 ? apiInfo?.data?.gaCoverImage?.url : schedule.tier === 1 - ? apiInfo?.data?.vipCoverImage?.url - : null) ?? apiInfo?.data?.coverImage.url + ? apiInfo?.data?.vipCoverImage?.url + : null) ?? apiInfo?.data?.coverImage.url return (
@@ -100,8 +100,8 @@ function EditionSchedule({ schedule }: { schedule: SuperMinterSchedule }) { {schedule.mode === 'VERIFY_MERKLE' ? 'Presale Limited Edition' : schedule.tier === 0 - ? 'Forever Edition' - : 'Limited Edition'} + ? 'Forever Edition' + : 'Limited Edition'} {coverImage ? Cover Image : null} diff --git a/package.json b/package.json index 95f9e2bd..1555b24b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "eslint-plugin-prettier": "^5.0.1", "husky": "^8.0.3", "lint-staged": "^15.0.1", - "prettier": "^3.0.3", + "prettier": "^3.1.1", "rimraf": "^5.0.5", "semver": "^7.5.4", "typescript": "5.2.2", diff --git a/packages/legacy-sdk/package.json b/packages/legacy-sdk/package.json index 1ff7c91f..44aee8db 100644 --- a/packages/legacy-sdk/package.json +++ b/packages/legacy-sdk/package.json @@ -30,9 +30,7 @@ "prepack": "node build.mjs", "prepare": "node build.mjs", "pull-env": "dotenv-vault pull", - "test": "vitest dev", - "test:cov": "vitest dev --coverage", - "test:ci": "CI=true vitest --coverage", + "test": "vitest", "tsc": "tsc -p tsconfig.build.json" }, "dependencies": { diff --git a/packages/legacy-sdk/src/client/edition/mint.ts b/packages/legacy-sdk/src/client/edition/mint.ts index f3734cba..84bab4c2 100644 --- a/packages/legacy-sdk/src/client/edition/mint.ts +++ b/packages/legacy-sdk/src/client/edition/mint.ts @@ -162,14 +162,14 @@ async function mintHelper( abi: minterAbiMap[interfaceId], }) : interfaceId === interfaceIds.IMerkleDropMinterV2 - ? client.readContract({ - ...params, - abi: minterAbiMap[interfaceId], - }) - : client.readContract({ - ...params, - abi: minterAbiMap[interfaceId], - })) + ? client.readContract({ + ...params, + abi: minterAbiMap[interfaceId], + }) + : client.readContract({ + ...params, + abi: minterAbiMap[interfaceId], + })) proof = await getMerkleProof.call(this, { merkleRoot, diff --git a/packages/legacy-sdk/src/types.ts b/packages/legacy-sdk/src/types.ts index 4e4113d0..d79404ba 100644 --- a/packages/legacy-sdk/src/types.ts +++ b/packages/legacy-sdk/src/types.ts @@ -113,8 +113,8 @@ export declare type PromiseOrValue = T | Promise type TupleSplit = O['length'] extends N ? [O, T] : T extends readonly [infer F, ...infer R] - ? TupleSplit - : [O, T] + ? TupleSplit + : [O, T] export type TakeFirst = TupleSplit[0] diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c7109dd3..4566f580 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -35,6 +35,8 @@ "prepack": "node build.mjs", "prepare": "node build.mjs", "postpublish": "gh-release", + "test": "vitest", + "tsc": "tsc -p tsconfig.build.json", "validate": "graphql-inspector validate \"./graphql/*.gql\" \"./schema.graphql\" --deprecated" }, "devDependencies": { @@ -49,7 +51,7 @@ "bob-watch": "^0.1.2", "concurrently": "^8.2.1", "esbuild": "^0.19.4", - "prettier": "^3.0.3", + "prettier": "^3.1.1", "typescript": "5.2.2", "viem": "^1.20.0", "zod": "^3.22.4" diff --git a/packages/sdk/src/contract/edition-v2/write/mint.ts b/packages/sdk/src/contract/edition-v2/write/mint.ts index 9c3710cc..3ea664b1 100644 --- a/packages/sdk/src/contract/edition-v2/write/mint.ts +++ b/packages/sdk/src/contract/edition-v2/write/mint.ts @@ -1,17 +1,30 @@ -import type { Chain, WalletClient } from 'viem' +import { encodeFunctionData, type WalletClient } from 'viem' import { curry } from '../../../utils/helpers' import type { EditionMintContractInput } from '../read/mint' +import { cdCompress } from '../../../utils/calldata' -export function editionMint>( +export function editionMint>( client: Client, { input }: EditionMintContractInput, ) { - return client.writeContract(input) + const calldata = encodeFunctionData({ abi: input.abi, functionName: input.functionName, args: input.args }) + const compressedCalldata = cdCompress(calldata) + + return client.sendTransaction({ + account: input.account, + chain: input.chain, + value: input.value, + to: input.address, + data: compressedCalldata, + gas: input.gas, + maxFeePerGas: input.maxFeePerGas, + maxPriorityFeePerGas: input.maxPriorityFeePerGas, + }) } -export function editionV2WalletActionsMint & { editionV2?: {} }>( - client: Client, -) { +export function editionV2WalletActionsMint< + Client extends Pick & { editionV2?: {} }, +>(client: Client) { return { editionV2: { ...client.editionV2, diff --git a/packages/sdk/src/contract/version.ts b/packages/sdk/src/contract/version.ts index 9bdbcfe6..0a41c186 100644 --- a/packages/sdk/src/contract/version.ts +++ b/packages/sdk/src/contract/version.ts @@ -19,6 +19,7 @@ export function soundEditionVersionPublicActions { + test('LibZip: Calldata compress', () => { + const data = + '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000a40c49ccbe000000000000000000000000000000000000000000000000000000000005b70e00000000000000000000000000000000000000000000000000000dfc79825feb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000645c48a7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000084fc6f7865000000000000000000000000000000000000000000000000000000000005b70e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000000000000000000000000000000ffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004449404b7c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f1cdf1a632eaaab40d1c263edf49faf749010a1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064df2ab5bb0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c3160700000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f1cdf1a632eaaab40d1c263edf49faf749010a100000000000000000000000000000000000000000000000000000000' + const expected = + '0x5369af27001e20001e04001e80001d0160001d0220001d02a0001ea40c49ccbe001c05b70e00190dfc79825feb005b645c48a7003a84fc6f7865001c05b70e002f008f000f008f003a4449404b7c002b1f1cdf1a632eaaab40d1c263edf49faf749010a1003a64df2ab5bb000b7f5c764cbc14f9669b88837ca1490cca17c31607002b1f1cdf1a632eaaab40d1c263edf49faf749010a1001b' + expect(cdCompress(data)).toEqual(expected) + }) +}) + +describe('cdDecompress', () => { + test('LibZip: Calldata decompress on invalid input', () => { + const data = '0xffffffff00ff' + const expected = + '0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + expect(cdDecompress(data)).toEqual(expected) + }) +}) diff --git a/packages/sdk/src/utils/calldata.ts b/packages/sdk/src/utils/calldata.ts new file mode 100644 index 00000000..fd60c808 --- /dev/null +++ b/packages/sdk/src/utils/calldata.ts @@ -0,0 +1,106 @@ +/* eslint-disable no-bitwise */ + +import type { Hex } from 'viem' + +/** + * typescript adaptations of cdCompress and cdDecompress from + * https://github.com/Vectorized/solady/blob/main/js/solady.js + * + * these are custom calldata compression and decompression functions + * that are supported by Sound contracts (sound creator v2, sound edition v2, and super minter) + * and can reduce calldata size (biggest influence on L2 gas costs) when there are lots of zeros or lots of 0xffs + */ +export function cdCompress(original: Hex): Hex { + const data = hexString(original) + let output: Hex = '0x' + let zeroCount: number = 0 + let ffCount: number = 0 + + const pushByte = (b: number): void => { + output += byteToString((+(output.length < 4 * 2 + 2) * 0xff) ^ b) + } + + const rle = (value: number, distance: number): void => { + pushByte(0x00) + pushByte(distance - 1 + value * 0x80) + } + + for (let i = 0; i < data.length; i += 2) { + const currentByte: number = parseByte(data, i) + if (currentByte === 0) { + if (ffCount) { + rle(1, ffCount) + ffCount = 0 + } + if (++zeroCount === 0x80) { + rle(0, 0x80) + zeroCount = 0 + } + continue + } + if (currentByte === 0xff) { + if (zeroCount) { + rle(0, zeroCount) + zeroCount = 0 + } + if (++ffCount === 0x20) { + rle(1, 0x20) + ffCount = 0 + } + continue + } + if (ffCount) { + rle(1, ffCount) + ffCount = 0 + } + if (zeroCount) { + rle(0, zeroCount) + zeroCount = 0 + } + pushByte(currentByte) + } + if (ffCount) { + rle(1, ffCount) + } + if (zeroCount) { + rle(0, zeroCount) + } + return output +} + +export function cdDecompress(compressed: Hex): Hex { + const data = hexString(compressed) + let output: Hex = '0x' + + for (let i = 0; i < data.length; ) { + let c: number = (+(i < 4 * 2) * 0xff) ^ parseByte(data, i) + i += 2 + if (!c) { + c = (+(i < 4 * 2) * 0xff) ^ parseByte(data, i) + const size: number = (c & 0x7f) + 1 + i += 2 + for (let j = 0; j < size; ++j) { + output += byteToString((c >> 7 !== 0 && j < 32 ? 1 : 0) * 0xff) + } + continue + } + output += byteToString(c) + } + return output +} + +function hexString(data: Hex): string { + const match = data.trim().match(/^(0x)?([0-9A-Fa-f]*)$/) + if (match && match[2] && match[2].length % 2 === 0) { + return match[2] + } + throw new Error('Data must be a valid hex string with even length.') +} + +function byteToString(b: number): string { + return (b | 0x100).toString(16).substr(1) +} + +function parseByte(data: string, i: number): number { + return parseInt(data.substr(i, 2), 16) +} diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts new file mode 100644 index 00000000..a842b332 --- /dev/null +++ b/packages/sdk/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + benchmark: { + outputFile: './bench/report.json', + reporters: process.env['CI'] ? ['json'] : ['verbose'], + }, + coverage: { + reporter: process.env['CI'] ? ['lcov'] : ['text', 'json', 'html'], + exclude: ['**/errors/utils.ts', '**/dist/**', '**/*.test.ts', '**/_test/**'], + }, + environment: 'node', + // setupFiles: ['./test/setup.ts'], + // globalSetup: ['./test/globalSetup.ts'], + testTimeout: 30_000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8630fa04..113e5c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,7 +69,7 @@ importers: specifier: ^15.0.1 version: 15.2.0 prettier: - specifier: ^3.0.3 + specifier: ^3.1.1 version: 3.1.1 rimraf: specifier: ^5.0.5 @@ -255,7 +255,7 @@ importers: specifier: ^0.19.4 version: 0.19.9 prettier: - specifier: ^3.0.3 + specifier: ^3.1.1 version: 3.1.1 typescript: specifier: 5.2.2 diff --git a/tsconfig.json b/tsconfig.json index dfa78d1d..20fada97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -103,5 +103,5 @@ "include": ["packages"], // subgraph typechecking is not necessary since the build scripts there convert AS code - "exclude": ["node_modules", "packages/subgraph"] + "exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"] }