Skip to content

[WIP] ERC20 assets #7321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-carrots-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

ERC20 assets
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -78,6 +78,5 @@
"wallet-ui": "turbo run dev --filter=./apps/wallet-ui --filter=./packages/thirdweb --filter=./packages/insight --filter=./packages/engine --filter=./packages/nebula",
"wallet-ui:build": "turbo run build --filter=./apps/wallet-ui --filter=./packages/thirdweb --filter=./packages/insight --filter=./packages/engine --filter=./packages/nebula"
},

"version": "1.0.0"
}
110 changes: 59 additions & 51 deletions packages/thirdweb/package.json
Original file line number Diff line number Diff line change
@@ -97,128 +97,133 @@
},
"exports": {
".": {
"types": "./dist/types/exports/thirdweb.d.ts",
"default": "./dist/cjs/exports/thirdweb.js",
"import": "./dist/esm/exports/thirdweb.js",
"default": "./dist/cjs/exports/thirdweb.js"
"types": "./dist/types/exports/thirdweb.d.ts"
},
"./adapters/*": {
"types": "./dist/types/exports/adapters/*.d.ts",
"default": "./dist/cjs/exports/adapters/*.js",
"import": "./dist/esm/exports/adapters/*.js",
"default": "./dist/cjs/exports/adapters/*.js"
"types": "./dist/types/exports/adapters/*.d.ts"
},
"./ai": {
"types": "./dist/types/exports/ai.d.ts",
"default": "./dist/cjs/exports/ai.js",
"import": "./dist/esm/exports/ai.js",
"default": "./dist/cjs/exports/ai.js"
"types": "./dist/types/exports/ai.d.ts"
},
"./assets": {
"default": "./dist/cjs/exports/assets.js",
"import": "./dist/esm/exports/assets.js",
"types": "./dist/types/exports/assets.d.ts"
},
"./auth": {
"types": "./dist/types/exports/auth.d.ts",
"default": "./dist/cjs/exports/auth.js",
"import": "./dist/esm/exports/auth.js",
"default": "./dist/cjs/exports/auth.js"
"types": "./dist/types/exports/auth.d.ts"
},
"./bridge": {
"types": "./dist/types/exports/bridge.d.ts",
"default": "./dist/cjs/exports/bridge.js",
"import": "./dist/esm/exports/bridge.js",
"default": "./dist/cjs/exports/bridge.js"
"types": "./dist/types/exports/bridge.d.ts"
},
"./chains": {
"types": "./dist/types/exports/chains.d.ts",
"default": "./dist/cjs/exports/chains.js",
"import": "./dist/esm/exports/chains.js",
"default": "./dist/cjs/exports/chains.js"
"types": "./dist/types/exports/chains.d.ts"
},
"./contract": {
"types": "./dist/types/exports/contract.d.ts",
"default": "./dist/cjs/exports/contract.js",
"import": "./dist/esm/exports/contract.js",
"default": "./dist/cjs/exports/contract.js"
"types": "./dist/types/exports/contract.d.ts"
},
"./deploys": {
"types": "./dist/types/exports/deploys.d.ts",
"default": "./dist/cjs/exports/deploys.js",
"import": "./dist/esm/exports/deploys.js",
"default": "./dist/cjs/exports/deploys.js"
"types": "./dist/types/exports/deploys.d.ts"
},
"./engine": {
"types": "./dist/types/exports/engine.d.ts",
"default": "./dist/cjs/exports/engine.js",
"import": "./dist/esm/exports/engine.js",
"default": "./dist/cjs/exports/engine.js"
"types": "./dist/types/exports/engine.d.ts"
},
"./event": {
"types": "./dist/types/exports/event.d.ts",
"default": "./dist/cjs/exports/event.js",
"import": "./dist/esm/exports/event.js",
"default": "./dist/cjs/exports/event.js"
"types": "./dist/types/exports/event.d.ts"
},
"./extensions/*": {
"types": "./dist/types/exports/extensions/*.d.ts",
"default": "./dist/cjs/exports/extensions/*.js",
"import": "./dist/esm/exports/extensions/*.js",
"default": "./dist/cjs/exports/extensions/*.js"
"types": "./dist/types/exports/extensions/*.d.ts"
},
"./insight": {
"types": "./dist/types/exports/insight.d.ts",
"default": "./dist/cjs/exports/insight.js",
"import": "./dist/esm/exports/insight.js",
"default": "./dist/cjs/exports/insight.js"
"types": "./dist/types/exports/insight.d.ts"
},
"./modules": {
"types": "./dist/types/exports/modules.d.ts",
"default": "./dist/cjs/exports/modules.js",
"import": "./dist/esm/exports/modules.js",
"default": "./dist/cjs/exports/modules.js"
"types": "./dist/types/exports/modules.d.ts"
},
"./package.json": "./package.json",
"./pay": {
"types": "./dist/types/exports/pay.d.ts",
"default": "./dist/cjs/exports/pay.js",
"import": "./dist/esm/exports/pay.js",
"default": "./dist/cjs/exports/pay.js"
"types": "./dist/types/exports/pay.d.ts"
},
"./react": {
"types": "./dist/types/exports/react.d.ts",
"react-native": "./dist/esm/exports/react.native.js",
"default": "./dist/cjs/exports/react.js",
"import": "./dist/esm/exports/react.js",
"default": "./dist/cjs/exports/react.js"
"react-native": "./dist/esm/exports/react.native.js",
"types": "./dist/types/exports/react.d.ts"
},
"./react-native": {
"types": "./dist/types/exports/react.native.d.ts",
"default": "./dist/cjs/exports/react.native.js",
"import": "./dist/esm/exports/react.native.js",
"default": "./dist/cjs/exports/react.native.js"
"types": "./dist/types/exports/react.native.d.ts"
},
"./rpc": {
"types": "./dist/types/exports/rpc.d.ts",
"default": "./dist/cjs/exports/rpc.js",
"import": "./dist/esm/exports/rpc.js",
"default": "./dist/cjs/exports/rpc.js"
"types": "./dist/types/exports/rpc.d.ts"
},
"./social": {
"types": "./dist/types/exports/social.d.ts",
"default": "./dist/cjs/exports/social.js",
"import": "./dist/esm/exports/social.js",
"default": "./dist/cjs/exports/social.js"
"types": "./dist/types/exports/social.d.ts"
},
"./storage": {
"types": "./dist/types/exports/storage.d.ts",
"default": "./dist/cjs/exports/storage.js",
"import": "./dist/esm/exports/storage.js",
"default": "./dist/cjs/exports/storage.js"
"types": "./dist/types/exports/storage.d.ts"
},
"./transaction": {
"types": "./dist/types/exports/transaction.d.ts",
"default": "./dist/cjs/exports/transaction.js",
"import": "./dist/esm/exports/transaction.js",
"default": "./dist/cjs/exports/transaction.js"
"types": "./dist/types/exports/transaction.d.ts"
},
"./utils": {
"types": "./dist/types/exports/utils.d.ts",
"default": "./dist/cjs/exports/utils.js",
"import": "./dist/esm/exports/utils.js",
"default": "./dist/cjs/exports/utils.js"
"types": "./dist/types/exports/utils.d.ts"
},
"./wallets": {
"types": "./dist/types/exports/wallets.d.ts",
"react-native": "./dist/esm/exports/wallets.native.js",
"default": "./dist/cjs/exports/wallets.js",
"import": "./dist/esm/exports/wallets.js",
"default": "./dist/cjs/exports/wallets.js"
"react-native": "./dist/esm/exports/wallets.native.js",
"types": "./dist/types/exports/wallets.d.ts"
},
"./wallets/*": {
"types": "./dist/types/exports/wallets/*.d.ts",
"default": "./dist/cjs/exports/wallets/*.js",
"import": "./dist/esm/exports/wallets/*.js",
"default": "./dist/cjs/exports/wallets/*.js"
"types": "./dist/types/exports/wallets/*.d.ts"
},
"./wallets/in-app": {
"types": "./dist/types/exports/wallets/in-app.d.ts",
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
"default": "./dist/cjs/exports/wallets/in-app.js",
"import": "./dist/esm/exports/wallets/in-app.js",
"default": "./dist/cjs/exports/wallets/in-app.js"
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
"types": "./dist/types/exports/wallets/in-app.d.ts"
}
},
"files": [
@@ -347,6 +352,9 @@
"ai": [
"./dist/types/exports/ai.d.ts"
],
"assets": [
"./dist/types/exports/assets.d.ts"
],
"auth": [
"./dist/types/exports/auth.d.ts"
],
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
"function initialize(address _owner, address _router, address _rewardLocker)",
"function setRouter(address router)",
"function getRouter() external view returns (address router)",
"function setRewardLocker(address rewardLocker)",
"function getRewardLocker() external view returns (address rewardLocker)",
"function addImplementation((bytes32 contractId, address implementation, uint8 implementationType, uint8 createHook, bytes createHookData) config, bool isDefault)",
"function getImplementation(bytes32 contractId) external view returns ((bytes32 contractId, address implementation, uint8 implementationType, uint8 createHook, bytes createHookData))",
"function createAsset(address creator, (uint256 amount, address referrer, bytes32 salt, bytes data, bytes hookData) createParams) external returns (address asset)",
"function createAssetById(bytes32 contractId, address creator, (uint256 amount, address referrer, bytes32 salt, bytes data, bytes hookData) params) public returns (address asset)",
"function createAssetByImplementationConfig((bytes32 contractId, address implementation, uint8 implementationType, uint8 createHook, bytes createHookData) config, address creator, (uint256 amount, address referrer, bytes32 salt, bytes data, bytes hookData) params) external returns (address asset)",
"function buyAsset(address asset, (address recipient, address referrer, address tokenIn, uint256 amountIn, uint256 minAmountOut, uint256 deadline, bytes data) params) external payable returns (uint256 amountIn, uint256 amountOut)",
"function sellAsset(address asset, (address recipient, address tokenOut, uint256 amountIn, uint256 minAmountOut, uint256 deadline, bytes data) params) external returns (uint256 amountIn, uint256 amountOut)",
"function listAsset(address asset, (address tokenIn, uint256 price, uint256 duration, bytes data) params) external",
"function distributeAsset(address asset, (uint256 amount, address recipient)[] contents) external payable",
"event ImplementationAdded(bytes32 contractId, address indexed implementation, uint8 implementationType, uint8 createHook, bytes32 createHookData)",
"event RouterUpdated(address indexed router)",
"event RewardLockerUpdated(address indexed locker)",
"event AssetCreated(bytes32 contractId, address indexed creator, address indexed asset, address referrer, bytes aux)",
"event AssetDistributed(address asset, uint256 recipientCount, uint256 totalAmount)",
"error InvalidValue()",
"error InvalidContractId()",
"error ValueTransferFailed()",
"error ArrayLengthMismatch()",
"error AssetNotRegistered()",
"error InvalidCreator()",
"error InvalidInitializer()",
"error InvalidImplementation()",
"error InvalidDeploymentArgs()",
"error InvalidCreateHook()",
"error CreateHookFailed()",
"error CreateHookReverted(string reason)",
"error ImplementationAlreadyExists()"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"function deployInfraProxyDeterministic(address implementation, bytes data, bytes32 salt, bytes extraData) public returns (address deployedProxy)",
"event AssetInfraDeployed(address indexed implementation, address indexed proxy, bytes32 inputSalt, bytes data, bytes extraData)",
"error ProxyDeploymentFailed()"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function initialize(string _name, string _symbol, string _contractURI, uint256 _maxSupply, address _owner) external"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function initialize(address _owner, address _feeRecipient, uint96 _defaultFee) external"
]
3 changes: 3 additions & 0 deletions packages/thirdweb/scripts/generate/abis/assets/Router.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"function initialize(address _owner) external"
]
331 changes: 331 additions & 0 deletions packages/thirdweb/src/assets/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import { encodePacked } from "viem";
import { ZERO_ADDRESS } from "../constants/addresses.js";
import { getContract } from "../contract/contract.js";
import { getOrDeployInfraContract } from "../contract/deployment/utils/bootstrap.js";
import {
deployCreate2Factory,
getDeployedCreate2Factory,
} from "../contract/deployment/utils/create-2-factory.js";
import { getDeployedInfraContract } from "../contract/deployment/utils/infra.js";
import { parseEventLogs } from "../event/actions/parse-logs.js";
import { assetInfraDeployedEvent } from "../extensions/assets/__generated__/AssetInfraDeployer/events/AssetInfraDeployed.js";
import { deployInfraProxyDeterministic } from "../extensions/assets/__generated__/AssetInfraDeployer/write/deployInfraProxyDeterministic.js";
import { encodeInitialize as encodeFeeManagerInit } from "../extensions/assets/__generated__/FeeManager/write/initialize.js";
import { encodeInitialize as encodeRouterInit } from "../extensions/assets/__generated__/Router/write/initialize.js";
import { sendAndConfirmTransaction } from "../transaction/actions/send-and-confirm-transaction.js";
import { keccakId } from "../utils/any-evm/keccak-id.js";
import { isContractDeployed } from "../utils/bytecode/is-contract-deployed.js";
import { keccak256 } from "../utils/hashing/keccak256.js";
import type {
ClientAndChain,
ClientAndChainAndAccount,
} from "../utils/types.js";
import {
DEFAULT_FEE_BPS,
DEFAULT_FEE_RECIPIENT,
DEFAULT_INFRA_ADMIN,
DEFAULT_SALT,
IMPLEMENTATIONS,
} from "./constants.js";
import { deployInfraProxy } from "./deploy-infra-proxy.js";
import { getInitCodeHashERC1967 } from "./get-initcode-hash-1967.js";

export async function deployRouter(options: ClientAndChainAndAccount) {
let [feeManager, marketSaleImpl] = await Promise.all([
getDeployedFeeManager(options),
getDeployedInfraContract({
...options,
contractId: "MarketSale",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
}),
]);

Check warning on line 41 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L33-L41

Added lines #L33 - L41 were not covered by tests

if (!feeManager) {
feeManager = await deployFeeManager(options);
}

Check warning on line 45 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L43-L45

Added lines #L43 - L45 were not covered by tests

if (!marketSaleImpl) {
marketSaleImpl = await getOrDeployInfraContract({
...options,
contractId: "MarketSale",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
}

Check warning on line 53 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L47-L53

Added lines #L47 - L53 were not covered by tests

const assetFactory = await getDeployedAssetFactory(options);
if (!assetFactory) {
throw new Error(`Asset factory not found for chain: ${options.chain.id}`);
}

Check warning on line 58 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L55-L58

Added lines #L55 - L58 were not covered by tests

const routerImpl = await getOrDeployInfraContract({
...options,
constructorParams: {
_feeManager: feeManager.address,
_marketSaleImplementation: marketSaleImpl.address,
},
contractId: "Router",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});

Check warning on line 68 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L60-L68

Added lines #L60 - L68 were not covered by tests

// encode init data
const initData = encodeRouterInit({
owner: DEFAULT_INFRA_ADMIN,
});

Check warning on line 73 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L71-L73

Added lines #L71 - L73 were not covered by tests

const routerProxyAddress = await deployInfraProxy({
...options,
assetFactory,
extraData: "0x",
implementationAddress: routerImpl.address,
initData,
});

Check warning on line 81 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L75-L81

Added lines #L75 - L81 were not covered by tests

return getContract({
address: routerProxyAddress,
chain: options.chain,
client: options.client,
});
}

Check warning on line 88 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L83-L88

Added lines #L83 - L88 were not covered by tests

export async function deployRewardLocker(options: ClientAndChainAndAccount) {
let v3PositionManager = ZERO_ADDRESS;
let v4PositionManager = ZERO_ADDRESS;

Check warning on line 92 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L90-L92

Added lines #L90 - L92 were not covered by tests

const implementations = IMPLEMENTATIONS[options.chain.id];

Check warning on line 94 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L94

Added line #L94 was not covered by tests

if (implementations) {
v3PositionManager = implementations.V3PositionManager || ZERO_ADDRESS;
v4PositionManager = implementations.V4PositionManager || ZERO_ADDRESS;
}

Check warning on line 99 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L96-L99

Added lines #L96 - L99 were not covered by tests

let feeManager = await getDeployedFeeManager(options);

Check warning on line 101 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L101

Added line #L101 was not covered by tests

if (!feeManager) {
feeManager = await deployFeeManager(options);
}

Check warning on line 105 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L103-L105

Added lines #L103 - L105 were not covered by tests

return await getOrDeployInfraContract({
...options,
constructorParams: {
_feeManager: feeManager.address,
_v3PositionManager: v3PositionManager,
_v4PositionManager: v4PositionManager,
},
contractId: "RewardLocker",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
}

Check warning on line 117 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L107-L117

Added lines #L107 - L117 were not covered by tests

export async function deployFeeManager(options: ClientAndChainAndAccount) {
// asset factory
let assetFactory = await getDeployedAssetFactory(options);
if (!assetFactory) {
assetFactory = await deployAssetFactory(options);
}

// fee manager implementation
const feeManagerImpl = await getOrDeployInfraContract({
...options,
contractId: "FeeManager",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would put this in a constant with a big TODO to update it before merge

});

// encode init data
const initData = encodeFeeManagerInit({
defaultFee: DEFAULT_FEE_BPS,
feeRecipient: DEFAULT_FEE_RECIPIENT,
owner: DEFAULT_INFRA_ADMIN,
});

// fee manager proxy deployment
const transaction = deployInfraProxyDeterministic({
contract: assetFactory,
data: initData,
extraData: "0x",
implementation: feeManagerImpl.address,
salt: keccakId(DEFAULT_SALT),
});

const receipt = await sendAndConfirmTransaction({
account: options.account,
transaction,
});
const proxyEvent = assetInfraDeployedEvent();
const decodedEvent = parseEventLogs({
events: [proxyEvent],
logs: receipt.logs,
});

if (decodedEvent.length === 0 || !decodedEvent[0]) {
throw new Error(
`No AssetInfraDeployed event found in transaction: ${receipt.transactionHash}`,
);
}

Check warning on line 163 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L160-L163

Added lines #L160 - L163 were not covered by tests

const feeManagerProxyAddress = decodedEvent[0]?.args.proxy;

return getContract({
address: feeManagerProxyAddress,
chain: options.chain,
client: options.client,
});
}

export async function deployAssetFactory(options: ClientAndChainAndAccount) {
// create2 factory
const create2Factory = await getDeployedCreate2Factory(options);
if (!create2Factory) {
await deployCreate2Factory(options);
}

Check warning on line 179 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L178-L179

Added lines #L178 - L179 were not covered by tests

// asset factory
return getOrDeployInfraContract({
...options,
contractId: "AssetInfraDeployer",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
}

export async function getDeployedRouter(options: ClientAndChain) {
const [feeManager, marketSaleImpl, assetFactory] = await Promise.all([
getDeployedFeeManager(options),
getDeployedInfraContract({
...options,
contractId: "MarketSale",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
}),
getDeployedAssetFactory(options),
]);

Check warning on line 198 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L189-L198

Added lines #L189 - L198 were not covered by tests

if (!feeManager || !marketSaleImpl || !assetFactory) {
return null;
}

Check warning on line 202 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L200-L202

Added lines #L200 - L202 were not covered by tests

const routerImpl = await getDeployedInfraContract({
...options,
constructorParams: {
_feeManager: feeManager.address,
_marketSaleImplementation: marketSaleImpl.address,
},
contractId: "Router",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});

Check warning on line 212 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L204-L212

Added lines #L204 - L212 were not covered by tests

if (!routerImpl) {
return null;
}

Check warning on line 216 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L214-L216

Added lines #L214 - L216 were not covered by tests

const initCodeHash = getInitCodeHashERC1967(routerImpl.address);

Check warning on line 218 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L218

Added line #L218 was not covered by tests

const saltHash = keccak256(
encodePacked(
["bytes32", "address"],
[keccakId(DEFAULT_SALT), DEFAULT_INFRA_ADMIN],
),
);

Check warning on line 225 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L220-L225

Added lines #L220 - L225 were not covered by tests

const hashedDeployInfo = keccak256(
encodePacked(
["bytes1", "address", "bytes32", "bytes32"],
["0xff", assetFactory.address, saltHash, initCodeHash],
),
);

Check warning on line 232 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L227-L232

Added lines #L227 - L232 were not covered by tests

const routerProxyAddress = `0x${hashedDeployInfo.slice(26)}`;
const routerProxy = getContract({
address: routerProxyAddress,
chain: options.chain,
client: options.client,
});

Check warning on line 239 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L234-L239

Added lines #L234 - L239 were not covered by tests

if (!(await isContractDeployed(routerProxy))) {
return null;
}

Check warning on line 243 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L241-L243

Added lines #L241 - L243 were not covered by tests

return routerProxy;
}

Check warning on line 246 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L245-L246

Added lines #L245 - L246 were not covered by tests

export async function getDeployedRewardLocker(options: ClientAndChain) {
let v3PositionManager = ZERO_ADDRESS;
let v4PositionManager = ZERO_ADDRESS;

Check warning on line 250 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L248-L250

Added lines #L248 - L250 were not covered by tests

const implementations = IMPLEMENTATIONS[options.chain.id];

Check warning on line 252 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L252

Added line #L252 was not covered by tests

if (implementations) {
v3PositionManager = implementations.V3PositionManager || ZERO_ADDRESS;
v4PositionManager = implementations.V4PositionManager || ZERO_ADDRESS;
}

Check warning on line 257 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L254-L257

Added lines #L254 - L257 were not covered by tests

const feeManager = await getDeployedFeeManager(options);

Check warning on line 259 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L259

Added line #L259 was not covered by tests

if (!feeManager) {
return null;
}

Check warning on line 263 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L261-L263

Added lines #L261 - L263 were not covered by tests

return await getDeployedInfraContract({
...options,
constructorParams: {
_feeManager: feeManager.address,
_v3PositionManager: v3PositionManager,
_v4PositionManager: v4PositionManager,
},
contractId: "RewardLocker",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
}

Check warning on line 275 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L265-L275

Added lines #L265 - L275 were not covered by tests

export async function getDeployedFeeManager(options: ClientAndChain) {
const [assetFactory, feeManagerImpl] = await Promise.all([
getDeployedAssetFactory(options),
getDeployedInfraContract({
...options,
contractId: "FeeManager",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
}),
]);

if (!assetFactory || !feeManagerImpl) {
return null;
}

Check warning on line 289 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L288-L289

Added lines #L288 - L289 were not covered by tests

const initCodeHash = getInitCodeHashERC1967(feeManagerImpl.address);

const saltHash = keccak256(
encodePacked(
["bytes32", "address"],
[keccakId(DEFAULT_SALT), DEFAULT_INFRA_ADMIN],
),
);

const hashedDeployInfo = keccak256(
encodePacked(
["bytes1", "address", "bytes32", "bytes32"],
["0xff", assetFactory.address, saltHash, initCodeHash],
),
);

const feeManagerProxyAddress = `0x${hashedDeployInfo.slice(26)}`;
const feeManagerProxy = getContract({
address: feeManagerProxyAddress,
chain: options.chain,
client: options.client,
});

if (!(await isContractDeployed(feeManagerProxy))) {
return null;
}

Check warning on line 316 in packages/thirdweb/src/assets/bootstrap.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/bootstrap.ts#L315-L316

Added lines #L315 - L316 were not covered by tests

return feeManagerProxy;
}

export async function getDeployedAssetFactory(args: ClientAndChain) {
const assetFactory = await getDeployedInfraContract({
...args,
contractId: "AssetInfraDeployer",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
if (!assetFactory) {
return null;
}
return assetFactory;
}
25 changes: 25 additions & 0 deletions packages/thirdweb/src/assets/bootstrapinfra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../test/src/chains.js";
import { TEST_CLIENT } from "../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../test/src/test-wallets.js";
import { deployFeeManager, getDeployedFeeManager } from "./bootstrap.js";

describe.runIf(process.env.TW_SECRET_KEY)("bootstrap asset infra", () => {
it("should bootstrap fee manager", async () => {
const feeManager = await deployFeeManager({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});

const expectedFeeManager = await getDeployedFeeManager({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});

expect(expectedFeeManager).toBeDefined();
expect(feeManager.address.toLowerCase()).to.equal(
expectedFeeManager?.address?.toLowerCase(),
);
});
});
38 changes: 38 additions & 0 deletions packages/thirdweb/src/assets/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const DEFAULT_MAX_SUPPLY_ERC20 = 10_000_000_000n;
export const DEFAULT_POOL_FEE = 10000;
export const DEFAULT_POOL_INITIAL_TICK = 230200;
export const DEFAULT_INFRA_ADMIN = "0x1a472863cf21d5aa27f417df9140400324c48f22";
export const DEFAULT_FEE_RECIPIENT =
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
export const DEFAULT_FEE_BPS = 50n;
export const DEFAULT_SALT = "thirdweb";

export const IMPLEMENTATIONS: Record<number, Record<string, string>> = {
8453: {
AssetEntrypointERC20: "0x7FF679bFb89ee0F88645CAb8Ab0844ea485a3434",
ERC20AssetImpl: "",
V3PositionManager: "",
V4PositionManager: "",
},
84532: {
AssetEntrypointERC20: "0x79C1236cFe59f1f088A15Da08b0D8667387d9703",
ERC20AssetImpl: "",
V3PositionManager: "",
V4PositionManager: "",
},
};

export enum ImplementationType {
CLONE = 0,
CLONE_WITH_IMMUTABLE_ARGS = 1,
ERC1967 = 2,
ERC1967_WITH_IMMUTABLE_ARGS = 3,
}

export enum CreateHook {
NONE = 0, // do nothing
CREATE_POOL = 1, // create a DEX pool via Router
CREATE_MARKET = 2, // create a market sale via Router
DISTRIBUTE = 3, // distribute tokens to recipients
EXTERNAL_HOOK = 4, // call an external hook contract
}
130 changes: 130 additions & 0 deletions packages/thirdweb/src/assets/create-token-by-impl-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { Hex } from "viem";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "../constants/addresses.js";
import { getContract } from "../contract/contract.js";
import { parseEventLogs } from "../event/actions/parse-logs.js";
import { assetCreatedEvent } from "../extensions/assets/__generated__/AssetEntrypointERC20/events/AssetCreated.js";
import { createAssetByImplementationConfig } from "../extensions/assets/__generated__/AssetEntrypointERC20/write/createAssetByImplementationConfig.js";
import { decimals } from "../extensions/erc20/read/decimals.js";
import { eth_blockNumber } from "../rpc/actions/eth_blockNumber.js";
import { getRpcClient } from "../rpc/rpc.js";
import { sendAndConfirmTransaction } from "../transaction/actions/send-and-confirm-transaction.js";
import { keccakId } from "../utils/any-evm/keccak-id.js";
import { toHex } from "../utils/encoding/hex.js";
import { toUnits } from "../utils/units.js";
import {
CreateHook,
DEFAULT_MAX_SUPPLY_ERC20,
ImplementationType,
} from "./constants.js";
import { getOrDeployEntrypointERC20 } from "./get-entrypoint-erc20.js";
import { getOrDeployERC20AssetImpl } from "./get-erc20-asset-impl.js";
import {
encodeInitParams,
encodeMarketConfig,
encodePoolConfig,
} from "./token-utils.js";
import type { CreateTokenOptions } from "./types.js";

export async function createTokenByImplConfig(options: CreateTokenOptions) {
const { client, chain, account, params, launchConfig } = options;

const creator = params.owner || account.address;

const encodedInitData = await encodeInitParams({
client,
creator,
params,
});

const rpcRequest = getRpcClient({
...options,
});
const blockNumber = await eth_blockNumber(rpcRequest);
const salt = options.salt
? options.salt.startsWith("0x") && options.salt.length === 66
? (options.salt as `0x${string}`)

Check warning on line 45 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L45

Added line #L45 was not covered by tests
: keccakId(options.salt)
: toHex(blockNumber, {
size: 32,
});

Check warning on line 49 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L47-L49

Added lines #L47 - L49 were not covered by tests

const entrypoint = await getOrDeployEntrypointERC20(options);
const tokenImpl = await getOrDeployERC20AssetImpl(options);

let hookData: Hex = "0x";
let amount = toUnits(
params.maxSupply.toString() || DEFAULT_MAX_SUPPLY_ERC20.toString(),
18,
);
if (launchConfig?.kind === "pool") {
hookData = encodePoolConfig(launchConfig.config);
amount = toUnits(
launchConfig.config.amount.toString() ||
DEFAULT_MAX_SUPPLY_ERC20.toString(),
18,
);

Check warning on line 65 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L60-L65

Added lines #L60 - L65 were not covered by tests
} else if (launchConfig?.kind === "market") {
const currencyContract =
launchConfig.config.tokenOut &&
launchConfig.config.tokenOut !== NATIVE_TOKEN_ADDRESS
? getContract({
address: launchConfig.config.tokenOut,
chain,
client,
})
: null;
const currencyDecimals = launchConfig.config.priceDenominator
? launchConfig.config.priceDenominator
: currencyContract
? await decimals({
contract: currencyContract,
})
: 18;

Check warning on line 82 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L67-L82

Added lines #L67 - L82 were not covered by tests

hookData = encodeMarketConfig({
...launchConfig.config,
decimals: currencyDecimals,
});
}

Check warning on line 88 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L84-L88

Added lines #L84 - L88 were not covered by tests

const transaction = createAssetByImplementationConfig({
config: {
contractId: keccakId("ERC20Asset"),
createHook:
launchConfig?.kind === "pool"
? CreateHook.CREATE_POOL

Check warning on line 95 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L95

Added line #L95 was not covered by tests
: launchConfig?.kind === "market"
? CreateHook.CREATE_MARKET

Check warning on line 97 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L97

Added line #L97 was not covered by tests
: launchConfig?.kind === "distribute"
? CreateHook.DISTRIBUTE

Check warning on line 99 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L99

Added line #L99 was not covered by tests
: CreateHook.NONE,
createHookData: hookData,
implementation: tokenImpl.address,
implementationType: ImplementationType.ERC1967,
},
contract: entrypoint,
creator,
params: {
amount,
data: encodedInitData,
hookData,
referrer: ZERO_ADDRESS,
salt,
},
});

const receipt = await sendAndConfirmTransaction({ account, transaction });
const assetEvent = assetCreatedEvent();
const decodedEvent = parseEventLogs({
events: [assetEvent],
logs: receipt.logs,
});

if (decodedEvent.length === 0 || !decodedEvent[0]) {
throw new Error(
`No AssetCreated event found in transaction: ${receipt.transactionHash}`,
);
}

Check warning on line 127 in packages/thirdweb/src/assets/create-token-by-impl-config.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/create-token-by-impl-config.ts#L124-L127

Added lines #L124 - L127 were not covered by tests

return decodedEvent[0]?.args.asset;
}
44 changes: 44 additions & 0 deletions packages/thirdweb/src/assets/create-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../test/src/chains.js";
import { TEST_CLIENT } from "../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../test/src/test-wallets.js";
import { getContract } from "../contract/contract.js";
import { name } from "../extensions/common/read/name.js";
// import { totalSupply } from "../extensions/erc20/__generated__/IERC20/read/totalSupply.js";
import { createTokenByImplConfig } from "./create-token-by-impl-config.js";

describe.runIf(process.env.TW_SECRET_KEY)("create token by impl config", () => {
it("should create token without pool", async () => {
const token = await createTokenByImplConfig({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
maxSupply: 10_00n,
name: "Test",
},
salt: "salt123",
});

expect(token).toBeDefined();

const tokenName = await name({
contract: getContract({
address: token,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
}),
});
expect(tokenName).to.eq("Test");

// const supply = await totalSupply({
// contract: getContract({
// client: TEST_CLIENT,
// chain: ANVIL_CHAIN,
// address: token,
// }),
// });

// console.log("supply: ", supply);
});
});
110 changes: 110 additions & 0 deletions packages/thirdweb/src/assets/create-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Hex } from "viem";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "../constants/addresses.js";
import { getContract } from "../contract/contract.js";
import { parseEventLogs } from "../event/actions/parse-logs.js";
import { assetCreatedEvent } from "../extensions/assets/__generated__/AssetEntrypointERC20/events/AssetCreated.js";
import { createAsset } from "../extensions/assets/__generated__/AssetEntrypointERC20/write/createAsset.js";
import { decimals } from "../extensions/erc20/read/decimals.js";
import { eth_blockNumber } from "../rpc/actions/eth_blockNumber.js";
import { getRpcClient } from "../rpc/rpc.js";
import { sendAndConfirmTransaction } from "../transaction/actions/send-and-confirm-transaction.js";
import { keccakId } from "../utils/any-evm/keccak-id.js";
import { toHex } from "../utils/encoding/hex.js";
import { toUnits } from "../utils/units.js";
import { DEFAULT_MAX_SUPPLY_ERC20 } from "./constants.js";
import { getOrDeployEntrypointERC20 } from "./get-entrypoint-erc20.js";
import {
encodeInitParams,
encodeMarketConfig,
encodePoolConfig,
} from "./token-utils.js";
import type { CreateTokenOptions } from "./types.js";

export async function createToken(options: CreateTokenOptions) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remember to generate the TSdoc for this with multiple examples

const { client, chain, account, params, launchConfig } = options;

const creator = params.owner || account.address;

const encodedInitData = await encodeInitParams({
client,
creator,
params,
});

const rpcRequest = getRpcClient({
...options,
});
const blockNumber = await eth_blockNumber(rpcRequest);
const salt = options.salt
? options.salt.startsWith("0x") && options.salt.length === 66
? (options.salt as `0x${string}`)
: keccakId(options.salt)
: toHex(blockNumber, {
size: 32,
});

const entrypoint = await getOrDeployEntrypointERC20(options);

let hookData: Hex = "0x";
let amount = toUnits(
params.maxSupply.toString() || DEFAULT_MAX_SUPPLY_ERC20.toString(),
18,
);
if (launchConfig?.kind === "pool") {
hookData = encodePoolConfig(launchConfig.config);
amount = toUnits(
launchConfig.config.amount.toString() ||
DEFAULT_MAX_SUPPLY_ERC20.toString(),
18,
);
} else if (launchConfig?.kind === "market") {
const currencyContract =
launchConfig.config.tokenOut &&
launchConfig.config.tokenOut !== NATIVE_TOKEN_ADDRESS
? getContract({
address: launchConfig.config.tokenOut,
chain,
client,
})
: null;
const currencyDecimals = launchConfig.config.priceDenominator
? launchConfig.config.priceDenominator
: currencyContract
? await decimals({
contract: currencyContract,
})
: 18;

hookData = encodeMarketConfig({
...launchConfig.config,
decimals: currencyDecimals,
});
}

const transaction = createAsset({
contract: entrypoint,
createParams: {
amount,
data: encodedInitData,
hookData,
referrer: ZERO_ADDRESS,
salt,
},
creator,
});

const receipt = await sendAndConfirmTransaction({ account, transaction });
const assetEvent = assetCreatedEvent();
const decodedEvent = parseEventLogs({
events: [assetEvent],
logs: receipt.logs,
});

if (decodedEvent.length === 0 || !decodedEvent[0]) {
throw new Error(
`No AssetCreated event found in transaction: ${receipt.transactionHash}`,
);
}

return decodedEvent[0]?.args.asset;
}
44 changes: 44 additions & 0 deletions packages/thirdweb/src/assets/deploy-infra-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Hex } from "viem";
import type { ThirdwebContract } from "../contract/contract.js";
import { parseEventLogs } from "../event/actions/parse-logs.js";
import { assetInfraDeployedEvent } from "../extensions/assets/__generated__/AssetInfraDeployer/events/AssetInfraDeployed.js";
import { deployInfraProxyDeterministic } from "../extensions/assets/__generated__/AssetInfraDeployer/write/deployInfraProxyDeterministic.js";
import { sendAndConfirmTransaction } from "../transaction/actions/send-and-confirm-transaction.js";
import { keccakId } from "../utils/any-evm/keccak-id.js";
import type { ClientAndChainAndAccount } from "../utils/types.js";
import { DEFAULT_SALT } from "./constants.js";

export async function deployInfraProxy(
options: ClientAndChainAndAccount & {
assetFactory: ThirdwebContract;
implementationAddress: string;
initData: Hex;
extraData: Hex;
},
) {
const transaction = deployInfraProxyDeterministic({
contract: options.assetFactory,
data: options.initData,
extraData: options.extraData,
implementation: options.implementationAddress,
salt: keccakId(DEFAULT_SALT),
});

const receipt = await sendAndConfirmTransaction({
account: options.account,
transaction,
});
const proxyEvent = assetInfraDeployedEvent();
const decodedEvent = parseEventLogs({
events: [proxyEvent],
logs: receipt.logs,
});

if (decodedEvent.length === 0 || !decodedEvent[0]) {
throw new Error(
`No AssetInfraDeployed event found in transaction: ${receipt.transactionHash}`,
);
}

Check warning on line 41 in packages/thirdweb/src/assets/deploy-infra-proxy.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/deploy-infra-proxy.ts#L38-L41

Added lines #L38 - L41 were not covered by tests

return decodedEvent[0]?.args.proxy;
}
116 changes: 116 additions & 0 deletions packages/thirdweb/src/assets/distribute-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { getContract, type ThirdwebContract } from "src/contract/contract.js";
import { getBalance } from "src/extensions/erc20/read/getBalance.js";
import { approve } from "src/extensions/erc20/write/approve.js";
import { sendAndConfirmTransaction } from "src/transaction/actions/send-and-confirm-transaction.js";
import { toUnits } from "src/utils/units.js";
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../test/src/chains.js";
import { TEST_CLIENT } from "../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
TEST_ACCOUNT_C,
TEST_ACCOUNT_D,
} from "../../test/src/test-wallets.js";
import { createTokenByImplConfig } from "./create-token-by-impl-config.js";
import { distributeToken } from "./distribute-token.js";
import { getDeployedEntrypointERC20 } from "./get-entrypoint-erc20.js";

describe.runIf(process.env.TW_SECRET_KEY)(
"create token by impl config",
{
timeout: 20000,
},
() => {
let token: ThirdwebContract;
beforeAll(async () => {
// create token
const tokenAddress = await createTokenByImplConfig({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
maxSupply: 10_000_000_000n,
name: "Test",
},
salt: "salt123",
});

token = getContract({
address: tokenAddress,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});

// approve tokens to entrypoint for distribution
const entrypoint = await getDeployedEntrypointERC20({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});

const approvalTx = approve({
amountWei: toUnits("1000", 18),
contract: token,
spender: entrypoint?.address as string,
});
await sendAndConfirmTransaction({
account: TEST_ACCOUNT_A,
transaction: approvalTx,
});
}, 20000);

it("should distrbute tokens to specified recipients", async () => {
const contents = [
{ amount: 10n, recipient: TEST_ACCOUNT_B.address },
{ amount: 15n, recipient: TEST_ACCOUNT_C.address },
{ amount: 20n, recipient: TEST_ACCOUNT_D.address },
];

const transaction = await distributeToken({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
contents,
tokenAddress: token.address,
});

await sendAndConfirmTransaction({ account: TEST_ACCOUNT_A, transaction });

const balanceB = (
await getBalance({
address: TEST_ACCOUNT_B.address,
contract: token,
})
).value;

const balanceC = (
await getBalance({
address: TEST_ACCOUNT_C.address,
contract: token,
})
).value;

const balanceD = (
await getBalance({
address: TEST_ACCOUNT_D.address,
contract: token,
})
).value;

// admin balance
const balanceA = (
await getBalance({
address: TEST_ACCOUNT_A.address,
contract: token,
})
).value;

expect(balanceB).to.equal(toUnits("10", 18));
expect(balanceC).to.equal(toUnits("15", 18));
expect(balanceD).to.equal(toUnits("20", 18));

expect(balanceA).to.equal(
toUnits("10000000000", 18) - balanceB - balanceC - balanceD,
);
});
},
);
26 changes: 26 additions & 0 deletions packages/thirdweb/src/assets/distribute-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { distributeAsset } from "../extensions/assets/__generated__/AssetEntrypointERC20/write/distributeAsset.js";
import type { ClientAndChain } from "../utils/types.js";
import { toUnits } from "../utils/units.js";
import { getDeployedEntrypointERC20 } from "./get-entrypoint-erc20.js";
import type { DistributeContent } from "./types.js";

type DistrbuteTokenParams = ClientAndChain & {
tokenAddress: string;
contents: DistributeContent[];
};

export async function distributeToken(options: DistrbuteTokenParams) {
const entrypoint = await getDeployedEntrypointERC20(options);

if (!entrypoint) {
throw new Error(`Entrypoint not found on chain: ${options.chain.id}`);
}

Check warning on line 17 in packages/thirdweb/src/assets/distribute-token.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/distribute-token.ts#L16-L17

Added lines #L16 - L17 were not covered by tests

return distributeAsset({
asset: options.tokenAddress,
contents: options.contents.map((a) => {
return { ...a, amount: toUnits(a.amount.toString(), 18) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the decimals always 18 here?

}),
contract: entrypoint,
});
}
123 changes: 123 additions & 0 deletions packages/thirdweb/src/assets/get-entrypoint-erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { encodePacked } from "viem";
import { ZERO_ADDRESS } from "../constants/addresses.js";
import { getContract, type ThirdwebContract } from "../contract/contract.js";
import { getOrDeployInfraContract } from "../contract/deployment/utils/bootstrap.js";
import { getDeployedInfraContract } from "../contract/deployment/utils/infra.js";
import { encodeInitialize } from "../extensions/assets/__generated__/AssetEntrypointERC20/write/initialize.js";
import { keccakId } from "../utils/any-evm/keccak-id.js";
import { isContractDeployed } from "../utils/bytecode/is-contract-deployed.js";
import { keccak256 } from "../utils/hashing/keccak256.js";
import type {
ClientAndChain,
ClientAndChainAndAccount,
} from "../utils/types.js";
import { deployAssetFactory, getDeployedAssetFactory } from "./bootstrap.js";
import {
DEFAULT_INFRA_ADMIN,
DEFAULT_SALT,
IMPLEMENTATIONS,
} from "./constants.js";
import { deployInfraProxy } from "./deploy-infra-proxy.js";
import { getInitCodeHashERC1967 } from "./get-initcode-hash-1967.js";

export async function getOrDeployEntrypointERC20(
options: ClientAndChainAndAccount,
): Promise<ThirdwebContract> {
const implementations = IMPLEMENTATIONS[options.chain.id];

if (implementations?.AssetEntrypointERC20) {
return getContract({
address: implementations.AssetEntrypointERC20,
chain: options.chain,
client: options.client,
});
}

Check warning on line 34 in packages/thirdweb/src/assets/get-entrypoint-erc20.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/get-entrypoint-erc20.ts#L29-L34

Added lines #L29 - L34 were not covered by tests

let assetFactory = await getDeployedAssetFactory(options);
if (!assetFactory) {
assetFactory = await deployAssetFactory(options);
}

const entrypointImpl = await getOrDeployInfraContract({
...options,
contractId: "AssetEntrypointERC20",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
version: "0.0.2",
});

// encode init data
const initData = encodeInitialize({
owner: DEFAULT_INFRA_ADMIN,
rewardLocker: ZERO_ADDRESS,
router: ZERO_ADDRESS,
});

const entyrpointProxyAddress = await deployInfraProxy({
...options,
assetFactory,
extraData: "0x",
implementationAddress: entrypointImpl.address,
initData,
});

return getContract({
address: entyrpointProxyAddress,
chain: options.chain,
client: options.client,
});
}

export async function getDeployedEntrypointERC20(options: ClientAndChain) {
const implementations = IMPLEMENTATIONS[options.chain.id];

if (implementations?.AssetEntrypointERC20) {
return getContract({
address: implementations.AssetEntrypointERC20,
chain: options.chain,
client: options.client,
});
}

Check warning on line 79 in packages/thirdweb/src/assets/get-entrypoint-erc20.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/get-entrypoint-erc20.ts#L74-L79

Added lines #L74 - L79 were not covered by tests

const [assetFactory, entrypointImpl] = await Promise.all([
getDeployedAssetFactory(options),
getDeployedInfraContract({
...options,
contractId: "AssetEntrypointERC20",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
version: "0.0.2",
}),
]);

if (!assetFactory || !entrypointImpl) {
return null;
}

Check warning on line 93 in packages/thirdweb/src/assets/get-entrypoint-erc20.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/get-entrypoint-erc20.ts#L92-L93

Added lines #L92 - L93 were not covered by tests

const initCodeHash = getInitCodeHashERC1967(entrypointImpl.address);

const saltHash = keccak256(
encodePacked(
["bytes32", "address"],
[keccakId(DEFAULT_SALT), DEFAULT_INFRA_ADMIN],
),
);

const hashedDeployInfo = keccak256(
encodePacked(
["bytes1", "address", "bytes32", "bytes32"],
["0xff", assetFactory.address, saltHash, initCodeHash],
),
);

const entrypointProxyAddress = `0x${hashedDeployInfo.slice(26)}`;
const entrypointProxy = getContract({
address: entrypointProxyAddress,
chain: options.chain,
client: options.client,
});

if (!(await isContractDeployed(entrypointProxy))) {
return null;
}

Check warning on line 120 in packages/thirdweb/src/assets/get-entrypoint-erc20.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/get-entrypoint-erc20.ts#L119-L120

Added lines #L119 - L120 were not covered by tests

return entrypointProxy;
}
24 changes: 24 additions & 0 deletions packages/thirdweb/src/assets/get-erc20-asset-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getContract, type ThirdwebContract } from "../contract/contract.js";
import { getOrDeployInfraContract } from "../contract/deployment/utils/bootstrap.js";
import type { ClientAndChainAndAccount } from "../utils/types.js";
import { IMPLEMENTATIONS } from "./constants.js";

export async function getOrDeployERC20AssetImpl(
options: ClientAndChainAndAccount,
): Promise<ThirdwebContract> {
const implementations = IMPLEMENTATIONS[options.chain.id];

if (implementations?.ERC20AssetImpl) {
return getContract({
address: implementations.ERC20AssetImpl,
chain: options.chain,
client: options.client,
});
}

Check warning on line 17 in packages/thirdweb/src/assets/get-erc20-asset-impl.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/get-erc20-asset-impl.ts#L12-L17

Added lines #L12 - L17 were not covered by tests

return await getOrDeployInfraContract({
...options,
contractId: "ERC20Asset",
publisher: "0x6453a486d52e0EB6E79Ec4491038E2522a926936",
});
}
8 changes: 8 additions & 0 deletions packages/thirdweb/src/assets/get-initcode-hash-1967.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { keccak256 } from "../utils/hashing/keccak256.js";

export function getInitCodeHashERC1967(implementation: string) {
// See `initCodeHashERC1967` - LibClone {https://github.com/vectorized/solady/blob/main/src/utils/LibClone.sol}
return keccak256(
`0x603d3d8160223d3973${implementation.toLowerCase().replace(/^0x/, "")}60095155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3`,
);
}
20 changes: 20 additions & 0 deletions packages/thirdweb/src/assets/is-router-enabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ZERO_ADDRESS } from "../constants/addresses.js";
import { getRouter } from "../extensions/assets/__generated__/AssetEntrypointERC20/read/getRouter.js";
import type { ClientAndChain } from "../utils/types.js";
import { getDeployedEntrypointERC20 } from "./get-entrypoint-erc20.js";

export async function isRouterEnabled(
options: ClientAndChain,
): Promise<boolean> {
const entrypoint = await getDeployedEntrypointERC20(options);

if (!entrypoint) {
return false;
}

const router = await getRouter({
contract: entrypoint,
});

return router !== ZERO_ADDRESS;
}
122 changes: 122 additions & 0 deletions packages/thirdweb/src/assets/token-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Hex } from "viem";
import type { ThirdwebClient } from "../client/client.js";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "../constants/addresses.js";
import { encodeInitialize } from "../extensions/assets/__generated__/ERC20Asset/write/initialize.js";
import { upload } from "../storage/upload.js";
import { encodeAbiParameters } from "../utils/abi/encodeAbiParameters.js";
import { toUnits } from "../utils/units.js";
import {
DEFAULT_MAX_SUPPLY_ERC20,
DEFAULT_POOL_FEE,
DEFAULT_POOL_INITIAL_TICK,
} from "./constants.js";
import type { MarketConfig, PoolConfig, TokenParams } from "./types.js";

export async function encodeInitParams(options: {
client: ThirdwebClient;
params: TokenParams;
creator: string;
}): Promise<Hex> {
const { client, params, creator } = options;

const contractURI =
options.params.contractURI ||
(await upload({
client,
files: [
{
description: params.description,
external_link: params.external_link,
image: params.image,
name: params.name,
social_urls: params.social_urls,
symbol: params.symbol,
},
],
})) ||
"";

Check warning on line 37 in packages/thirdweb/src/assets/token-utils.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/token-utils.ts#L37

Added line #L37 was not covered by tests

return encodeInitialize({
contractURI,
maxSupply: toUnits(
params.maxSupply.toString() || DEFAULT_MAX_SUPPLY_ERC20.toString(),
18,
),
name: params.name,
owner: creator,
symbol: params.symbol || params.name,
});
}

export function encodePoolConfig(poolConfig: PoolConfig): Hex {
const POOL_PARAMS = [
{
name: "currency",
type: "address",
},
{
name: "amount",
type: "uint256",
},
{
name: "fee",
type: "uint24",
},
{
name: "initialTick",
type: "uint24",
},
] as const;

Check warning on line 69 in packages/thirdweb/src/assets/token-utils.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/token-utils.ts#L52-L69

Added lines #L52 - L69 were not covered by tests

return encodeAbiParameters(POOL_PARAMS, [
poolConfig.currency || NATIVE_TOKEN_ADDRESS,
toUnits(poolConfig.amount.toString(), 18),
poolConfig.fee || DEFAULT_POOL_FEE,
poolConfig.initialTick || DEFAULT_POOL_INITIAL_TICK,
]);
}

Check warning on line 77 in packages/thirdweb/src/assets/token-utils.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/token-utils.ts#L71-L77

Added lines #L71 - L77 were not covered by tests

export function encodeMarketConfig(
marketConfig: MarketConfig & { decimals: number },
): Hex {
const MARKET_PARAMS = [
{
name: "tokenOut",
type: "address",
},
{
name: "pricePerUnit",
type: "uint256",
},
{
name: "priceDenominator",
type: "uint8",
},
{
name: "startTime",
type: "uint48",
},
{
name: "endTime",
type: "uint48",
},
{
name: "hook",
type: "address",
},
{
name: "hookInit",
type: "bytes",
},
] as const;

Check warning on line 111 in packages/thirdweb/src/assets/token-utils.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/token-utils.ts#L80-L111

Added lines #L80 - L111 were not covered by tests

return encodeAbiParameters(MARKET_PARAMS, [
marketConfig.tokenOut || NATIVE_TOKEN_ADDRESS,
marketConfig.pricePerUnit,
marketConfig.priceDenominator || marketConfig.decimals || 18,
marketConfig.startTime || 0,
marketConfig.endTime || 0,
ZERO_ADDRESS,
"0x",
]);
}

Check warning on line 122 in packages/thirdweb/src/assets/token-utils.ts

Codecov / codecov/patch

packages/thirdweb/src/assets/token-utils.ts#L113-L122

Added lines #L113 - L122 were not covered by tests
51 changes: 51 additions & 0 deletions packages/thirdweb/src/assets/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { FileOrBufferOrString } from "../storage/upload/types.js";
import type { ClientAndChainAndAccount } from "../utils/types.js";

export type TokenParams = {
name: string;
description?: string;
image?: FileOrBufferOrString;
external_link?: string;
social_urls?: Record<string, string>;
symbol?: string;
contractURI?: string;
maxSupply: bigint;
owner?: string;
};

export type PoolConfig = {
amount: bigint;
currency?: string;
fee?: number;
initialTick?: number;
};

export type MarketConfig = {
tokenOut?: string;
pricePerUnit: bigint;
priceDenominator?: number;
startTime?: number;
endTime?: number;
hookAddress?: string;
hookInitData?: string;
};

export type DistributeContent = {
amount: bigint;
recipient: string;
};

type DistributeConfig = {
content: DistributeContent[];
};

type LaunchConfig =
| { kind: "pool"; config: PoolConfig }
| { kind: "market"; config: MarketConfig }
| { kind: "distribute"; config: DistributeConfig };

export type CreateTokenOptions = ClientAndChainAndAccount & {
salt?: string;
params: TokenParams;
launchConfig?: LaunchConfig;
};
22 changes: 22 additions & 0 deletions packages/thirdweb/src/exports/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export {
deployAssetFactory,
deployFeeManager,
deployRewardLocker,
deployRouter,
getDeployedAssetFactory,
getDeployedFeeManager,
getDeployedRewardLocker,
getDeployedRouter,
} from "../assets/bootstrap.js";
export { createToken } from "../assets/create-token.js";
export { createTokenByImplConfig } from "../assets/create-token-by-impl-config.js";
export { distributeToken } from "../assets/distribute-token.js";
export { getDeployedEntrypointERC20 } from "../assets/get-entrypoint-erc20.js";
export { isRouterEnabled } from "../assets/is-router-enabled.js";
export type {
CreateTokenOptions,
DistributeContent,
MarketConfig,
PoolConfig,
TokenParams,
} from "../assets/types.js";
Loading