├── .nvmrc ├── .gitignore ├── src ├── chains │ ├── index.ts │ ├── near.ts │ └── ethereum.ts ├── types │ ├── index.ts │ ├── util.ts │ ├── guards.ts │ └── interfaces.ts ├── utils │ ├── index.ts │ ├── request.ts │ ├── kdf.ts │ ├── mock-sign.ts │ ├── transaction.ts │ └── signature.ts ├── index.ts ├── setup.ts ├── network │ ├── index.ts │ └── constants.ts └── mpcContract.ts ├── .env.example ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── .prettierrc ├── jest.config.json ├── examples ├── getEthAddress.ts ├── send-eth.ts ├── sign-message.ts ├── weth │ ├── wrap.ts │ └── unwrap.ts ├── nft │ ├── erc721 │ │ ├── mint.ts │ │ └── transfer.ts │ ├── setApprovalForAll.ts │ └── erc1155 │ │ └── transfer.ts ├── setup.ts ├── README.md ├── opensea.ts └── abis │ ├── WETH.json │ ├── ERC1155.json │ └── ERC721.json ├── tests ├── unit │ ├── network.test.ts │ ├── utils │ │ ├── mock-sign.test.ts │ │ ├── kdf.test.ts │ │ ├── transaction.test.ts │ │ └── signature.test.ts │ ├── chains.near.test.ts │ ├── ethereum.test.ts │ ├── types.util.test.ts │ ├── mpcContract.test.ts │ ├── index.test.ts │ ├── types.guards.test.ts │ └── wc.handlers.test.ts └── e2e.test.ts ├── .github └── workflows │ ├── publish.yaml │ ├── pull-request.yaml │ ├── e2e.yaml │ └── upgrade.yaml ├── eslint.config.cjs ├── LICENSE ├── package.json ├── tsconfig.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.13.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode/ 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /src/chains/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ethereum"; 2 | export * from "./near"; 3 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./interfaces"; 2 | export * from "./guards"; 3 | export * from "./util"; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEAR_ACCOUNT_ID= 2 | NEAR_ACCOUNT_PRIVATE_KEY= 3 | 4 | MPC_CONTRACT_ID=v1.signer-prod.testnet 5 | NETWORK=testnet 6 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | } 7 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kdf"; 2 | export * from "./request"; 3 | export * from "./signature"; 4 | export * from "./transaction"; 5 | 6 | export { mockAdapter } from "./mock-sign"; 7 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "outDir": "dist/esm", 7 | } 8 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// Directories 2 | export * from "./chains"; 3 | export * from "./network"; 4 | export * from "./types"; 5 | export * from "./utils"; 6 | /// Files 7 | export * from "./mpcContract"; 8 | export * from "./setup"; 9 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "coverageThreshold": { 5 | "global": { 6 | "branches": 60, 7 | "functions": 60, 8 | "lines": 60, 9 | "statements": 60 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/getEthAddress.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { setupNearEthAdapter } from "./setup"; 3 | dotenv.config(); 4 | 5 | const run = async (): Promise => { 6 | const evm = await setupNearEthAdapter(); 7 | console.log(`Your eth address: ${evm.address}`); 8 | }; 9 | 10 | run(); 11 | -------------------------------------------------------------------------------- /examples/send-eth.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "./setup"; 3 | dotenv.config(); 4 | 5 | const run = async (): Promise => { 6 | const evm = await setupNearEthAdapter(); 7 | await evm.signAndSendTransaction({ 8 | // Sending to self. 9 | to: evm.address, 10 | // THIS IS ONE WEI! 11 | value: 1n, 12 | chainId: SEPOLIA_CHAIN_ID, 13 | }); 14 | }; 15 | 16 | run(); 17 | -------------------------------------------------------------------------------- /examples/sign-message.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { setupNearEthAdapter } from "./setup"; 3 | dotenv.config(); 4 | 5 | const run = async (): Promise => { 6 | const evm = await setupNearEthAdapter(); 7 | const message = "Hello World"; 8 | console.log(`Signing "${message}" with ${evm.address}`); 9 | 10 | const signature = await evm.signMessage(message); 11 | console.log("Got Validated Signature", signature); 12 | }; 13 | 14 | run(); 15 | -------------------------------------------------------------------------------- /examples/weth/wrap.ts: -------------------------------------------------------------------------------- 1 | import { parseEther } from "viem"; 2 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../setup"; 3 | 4 | const run = async (): Promise => { 5 | const neareth = await setupNearEthAdapter(); 6 | const sepoliaWETH = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14"; 7 | const ethAmount = parseEther("0.01"); 8 | const deposit = "0xd0e30db0"; 9 | 10 | await neareth.signAndSendTransaction({ 11 | to: sepoliaWETH, 12 | value: ethAmount, 13 | data: deposit, 14 | chainId: SEPOLIA_CHAIN_ID, 15 | }); 16 | }; 17 | 18 | run(); 19 | -------------------------------------------------------------------------------- /tests/unit/network.test.ts: -------------------------------------------------------------------------------- 1 | import { isTestnet } from "../../src/"; 2 | describe("network", () => { 3 | it("isTestnet", async () => { 4 | expect(isTestnet(1)).toBe(false); 5 | expect(isTestnet(10)).toBe(false); 6 | expect(isTestnet(97)).toBe(true); 7 | expect(isTestnet(43114)).toBe(false); 8 | expect(isTestnet(100)).toBe(false); 9 | expect(isTestnet(137)).toBe(false); 10 | expect(isTestnet(10200)).toBe(true); 11 | expect(isTestnet(84532)).toBe(true); 12 | expect(isTestnet(421614)).toBe(true); 13 | expect(isTestnet(11155111)).toBe(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/nft/erc721/mint.ts: -------------------------------------------------------------------------------- 1 | import { encodeFunctionData } from "viem"; 2 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../../setup"; 3 | 4 | const run = async (): Promise => { 5 | const adapter = await setupNearEthAdapter(); 6 | 7 | await adapter.signAndSendTransaction({ 8 | to: "0xAA5FcF171dDf9FE59c985A28747e650C2e9069cA", 9 | data: encodeFunctionData({ 10 | abi: ["function safeMint(address to)"], 11 | functionName: "safeMint", 12 | args: ["0xAA5FcF171dDf9FE59c985A28747e650C2e9069cA"], 13 | }), 14 | chainId: SEPOLIA_CHAIN_ID, 15 | }); 16 | }; 17 | 18 | run(); 19 | -------------------------------------------------------------------------------- /examples/nft/setApprovalForAll.ts: -------------------------------------------------------------------------------- 1 | import erc721ABI from "../abis/ERC721.json"; 2 | import { encodeFunctionData } from "viem"; 3 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../setup"; 4 | 5 | const run = async (): Promise => { 6 | const neareth = await setupNearEthAdapter(); 7 | const tokenAddress = "0xe66be37f6b446079fe71a497312996dff6bd963f"; 8 | const operator = "0x8d99F8b2710e6A3B94d9bf465A98E5273069aCBd"; 9 | 10 | await neareth.signAndSendTransaction({ 11 | to: tokenAddress, 12 | data: encodeFunctionData({ 13 | abi: erc721ABI, 14 | functionName: "setApprovalForAll", 15 | args: [operator, true], 16 | }), 17 | chainId: SEPOLIA_CHAIN_ID, 18 | }); 19 | }; 20 | 21 | run(); 22 | -------------------------------------------------------------------------------- /examples/weth/unwrap.ts: -------------------------------------------------------------------------------- 1 | import wethABI from "../abis/WETH.json"; 2 | import { encodeFunctionData, parseEther } from "viem"; 3 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../setup"; 4 | 5 | const run = async (): Promise => { 6 | const neareth = await setupNearEthAdapter(); 7 | const sepoliaWETH = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14"; 8 | const withdrawAmount = 0.001; 9 | 10 | await neareth.signAndSendTransaction({ 11 | to: sepoliaWETH, 12 | // No eth is "attached" to a WETH withdraw. 13 | data: encodeFunctionData({ 14 | abi: wethABI, 15 | functionName: "withdraw", 16 | args: [parseEther(withdrawAmount.toString())], 17 | }), 18 | chainId: SEPOLIA_CHAIN_ID, 19 | }); 20 | }; 21 | 22 | run(); 23 | -------------------------------------------------------------------------------- /examples/nft/erc721/transfer.ts: -------------------------------------------------------------------------------- 1 | import erc721ABI from "../../abis/ERC721.json"; 2 | import { encodeFunctionData } from "viem"; 3 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../../setup"; 4 | 5 | const run = async (): Promise => { 6 | const neareth = await setupNearEthAdapter(); 7 | 8 | const tokenAddress = "0xb5EF4EbB25fCA7603C028610ddc9233d399dA34d"; 9 | const tokenId = 17; 10 | const to = "0x8d99F8b2710e6A3B94d9bf465A98E5273069aCBd"; 11 | 12 | await neareth.signAndSendTransaction({ 13 | to: tokenAddress, 14 | data: encodeFunctionData({ 15 | abi: erc721ABI, 16 | functionName: "safeTransferFrom(address,address,uint256)", 17 | args: [neareth.address, to, tokenId], 18 | }), 19 | chainId: SEPOLIA_CHAIN_ID, 20 | }); 21 | }; 22 | 23 | run(); 24 | -------------------------------------------------------------------------------- /tests/unit/utils/mock-sign.test.ts: -------------------------------------------------------------------------------- 1 | import { recoverMessageAddress } from "viem"; 2 | import { mockAdapter } from "../../../src/utils/mock-sign"; 3 | 4 | describe("Mock Signing", () => { 5 | it("MockAdapter", async () => { 6 | const adapter = await mockAdapter(); 7 | expect(adapter.address).toBe("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"); 8 | 9 | const message = "Hello Joe!"; 10 | const signature = await adapter.signMessage(message); 11 | 12 | expect(signature).toBe( 13 | "0xcadb9d3ade67e815c11646eea6cd52abb7f860af612cd914a2c64c01908af870246926e8d5774d0b63efc46cd9f77cc650a7ad923df69a5151805422ab1d625a1c" 14 | ); 15 | // Recover Address: 16 | const recoveredAddress = await recoverMessageAddress({ 17 | message, 18 | signature, 19 | }); 20 | expect(recoveredAddress).toBe(adapter.address); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/nft/erc1155/transfer.ts: -------------------------------------------------------------------------------- 1 | import erc1155Abi from "../../abis/ERC1155.json"; 2 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../../setup"; 3 | import { encodeFunctionData } from "viem"; 4 | 5 | const run = async (): Promise => { 6 | const evm = await setupNearEthAdapter(); 7 | // TODO retrieve from user: 8 | const tokenAddress = "0x284c37b0fcb72034ff25855da57fcf097b255474"; 9 | const tokenId = 1; 10 | const to = "0x8d99F8b2710e6A3B94d9bf465A98E5273069aCBd"; 11 | 12 | const callData = encodeFunctionData({ 13 | abi: erc1155Abi, 14 | functionName: "safeTransferFrom(address,address,uint256,uint256,bytes)", 15 | args: [evm.address, to, tokenId, 1, "0x"], 16 | }); 17 | 18 | await evm.signAndSendTransaction({ 19 | to: tokenAddress, 20 | data: callData, 21 | chainId: SEPOLIA_CHAIN_ID, 22 | }); 23 | }; 24 | 25 | run(); 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | registry-url: https://registry.npmjs.org/ 16 | 17 | - name: Setup Bun 18 | uses: oven-sh/setup-bun@v2 19 | with: 20 | bun-version: latest 21 | 22 | - name: Build & Set Package Version to Tag 23 | run: | 24 | bun install --frozen-lockfile 25 | bun run build 26 | VERSION=${GITHUB_REF#refs/tags/} 27 | npm version $VERSION --no-git-tag-version 28 | - name: Publish 29 | run: npm publish --access public 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 32 | -------------------------------------------------------------------------------- /examples/setup.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { NearEthAdapter, setupAdapter } from "../src"; 3 | 4 | // This is Sepolia, but can be replaced with nearly any EVM network. 5 | export const SEPOLIA_CHAIN_ID = 11_155_111; 6 | 7 | export async function setupNearEthAdapter(): Promise { 8 | dotenv.config(); 9 | const { NEAR_ACCOUNT_ID, NEAR_ACCOUNT_PRIVATE_KEY, MPC_CONTRACT_ID } = 10 | process.env; 11 | if (!(NEAR_ACCOUNT_ID && NEAR_ACCOUNT_PRIVATE_KEY && MPC_CONTRACT_ID)) { 12 | throw new Error( 13 | "One of env vars NEAR_ACCOUNT_ID, NEAR_ACCOUNT_PRIVATE_KEY, or MPC_CONTRACT_ID is undefined" 14 | ); 15 | } 16 | return setupAdapter({ 17 | accountId: NEAR_ACCOUNT_ID, 18 | privateKey: NEAR_ACCOUNT_PRIVATE_KEY, 19 | mpcContractId: MPC_CONTRACT_ID, 20 | }); 21 | } 22 | 23 | export function sleep(ms: number): Promise { 24 | return new Promise((resolve) => setTimeout(resolve, ms)); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | types: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Cache Bun dependencies 16 | uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.bun/install/cache 20 | node_modules 21 | packages/*/node_modules 22 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} 23 | restore-keys: | 24 | ${{ runner.os }}-bun- 25 | 26 | - name: Setup Bun 27 | uses: oven-sh/setup-bun@v2 28 | with: 29 | bun-version: latest 30 | 31 | - name: Install Dependencies 32 | run: bun install --frozen-lockfile 33 | 34 | - name: Install & Build 35 | run: | 36 | bun lint 37 | bun run build 38 | bun verify 39 | -------------------------------------------------------------------------------- /tests/unit/chains.near.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair } from "near-api-js"; 2 | import { 3 | nearAccountFromAccountId, 4 | nearAccountFromKeyPair, 5 | createNearAccount, 6 | } from "../../src/"; 7 | 8 | const TESTNET_CONFIG = { 9 | networkId: "testnet", 10 | nodeUrl: "https://rpc.testnet.near.org", 11 | }; 12 | describe("near", () => { 13 | it("createNearAccount", async () => { 14 | const id = "farmface.testnet"; 15 | const account = await createNearAccount(id, TESTNET_CONFIG); 16 | expect(account.accountId).toBe(id); 17 | }); 18 | 19 | it("nearAccountFromAccountId", async () => { 20 | const id = "farmface.testnet"; 21 | const account = await nearAccountFromAccountId(id, TESTNET_CONFIG); 22 | expect(account.accountId).toBe(id); 23 | }); 24 | 25 | it("nearAccountFromKeyPair", async () => { 26 | const id = "farmface.testnet"; 27 | const account = await nearAccountFromKeyPair({ 28 | accountId: id, 29 | network: TESTNET_CONFIG, 30 | keyPair: KeyPair.fromRandom("ed25519"), 31 | }); 32 | expect(account.accountId).toBe(id); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const tsPlugin = require("@typescript-eslint/eslint-plugin"); 2 | const tsParser = require("@typescript-eslint/parser"); 3 | const js = require("@eslint/js"); 4 | 5 | module.exports = Object.assign({}, js.configs.recommended, { 6 | files: ["**/*.ts", "**/*.tsx"], 7 | languageOptions: { 8 | ecmaVersion: "latest", 9 | sourceType: "module", 10 | parser: tsParser, 11 | parserOptions: { 12 | ecmaVersion: "latest", 13 | sourceType: "module", 14 | }, 15 | }, 16 | plugins: { 17 | "@typescript-eslint": tsPlugin, 18 | }, 19 | ignores: ["node_modules/*"], 20 | rules: Object.assign({}, tsPlugin.configs.rules, { 21 | indent: ["error", 2, { SwitchCase: 1 }], 22 | "linebreak-style": ["error", "unix"], 23 | quotes: ["error", "double"], 24 | semi: ["error", "always"], 25 | "@typescript-eslint/no-explicit-any": "error", 26 | "@typescript-eslint/explicit-function-return-type": [ 27 | "error", 28 | { 29 | allowExpressions: true, 30 | allowTypedFunctionExpressions: true, 31 | }, 32 | ], 33 | }), 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/ethereum.test.ts: -------------------------------------------------------------------------------- 1 | import { zeroAddress } from "viem"; 2 | import { setupAdapter, signMethods } from "../../src/"; 3 | 4 | const accountId = "farmface.testnet"; 5 | const network = { 6 | networkId: "testnet", 7 | nodeUrl: "https://rpc.testnet.near.org", 8 | }; 9 | const mpcContractId = "v1.signer-prod.testnet"; 10 | // disable logging on this file. 11 | console.log = () => null; 12 | describe("ethereum", () => { 13 | it("adapter (read) methods", async () => { 14 | const adapter = await setupAdapter({ 15 | accountId, 16 | network, 17 | mpcContractId, 18 | derivationPath: "ethereum,1", 19 | }); 20 | expect(await adapter.address).toBe( 21 | "0xe09907d0a59bf84a68f5249ab328fc0ce0417a28" 22 | ); 23 | expect(await adapter.getBalance(100)).toBe(0n); 24 | expect(adapter.nearAccountId()).toBe(accountId); 25 | const { transaction } = await adapter.createTxPayload({ 26 | to: zeroAddress, 27 | chainId: 11155111, 28 | }); 29 | 30 | const request = await adapter.mpcSignRequest(transaction); 31 | expect(request.actions.length).toEqual(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mintbase Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/unit/types.util.test.ts: -------------------------------------------------------------------------------- 1 | import { convertToAction, convertToCompatibleFormat } from "../../src/types"; 2 | describe("types/utils", () => { 3 | it("convertToAction", async () => { 4 | const action = convertToAction({ 5 | type: "FunctionCall", 6 | params: { 7 | methodName: "sign", 8 | args: { 9 | request: { 10 | path: "x", 11 | payload: [1], 12 | key_version: 1, 13 | }, 14 | }, 15 | gas: "1", 16 | deposit: "2", 17 | }, 18 | }); 19 | expect(action.functionCall).toBeDefined(); 20 | }); 21 | 22 | it("convertToCompatibleFormat success", async () => { 23 | const args = { 24 | request: { 25 | path: "x", 26 | payload: [1], 27 | key_version: 1, 28 | }, 29 | }; 30 | expect(convertToCompatibleFormat(args)).toBe(args); 31 | expect(convertToCompatibleFormat(1)).toStrictEqual( 32 | new TextEncoder().encode("1") 33 | ); 34 | }); 35 | 36 | it("convertToCompatibleFormat fails", async () => { 37 | expect(() => convertToCompatibleFormat(1n)).toThrow( 38 | "Failed to convert the input: JSON.stringify cannot serialize BigInt." 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: End to End Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | types: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: "22" 17 | 18 | - name: Cache Bun dependencies 19 | uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.bun/install/cache 23 | node_modules 24 | packages/*/node_modules 25 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} 26 | restore-keys: | 27 | ${{ runner.os }}-bun- 28 | 29 | - name: Setup Bun 30 | uses: oven-sh/setup-bun@v2 31 | with: 32 | bun-version: latest 33 | 34 | - name: Install Dependencies 35 | run: bun install --frozen-lockfile 36 | 37 | - name: E2E Test 38 | run: | 39 | bun test e2e 40 | env: 41 | MPC_CONTRACT_ID: v1.signer-prod.testnet 42 | NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}} 43 | NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}} 44 | ETH_PK: ${{secrets.ETH_PK}} 45 | -------------------------------------------------------------------------------- /.github/workflows/upgrade.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Bun Update 2 | 3 | on: 4 | schedule: 5 | # Runs at 12 am on the 1th day of every month 6 | - cron: "0 0 1 * *" 7 | workflow_dispatch: # This allows manual triggering 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | upgrade: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: "22" 25 | 26 | - name: Install and upgrade dependencies 27 | run: | 28 | bun i 29 | bun update 30 | 31 | - name: Commit changes 32 | run: | 33 | git config --global user.name "github-actions[bot]" 34 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 35 | git add . 36 | git commit -m "upgrade dependencies" || echo "No changes to commit" 37 | 38 | - name: Create Pull Request 39 | uses: peter-evans/create-pull-request@v7 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | commit-message: upgrade dependencies 43 | branch: auto-upgrade-branch 44 | title: "Bun Update" 45 | body: "This PR updates dependencies via `bun update`" 46 | -------------------------------------------------------------------------------- /src/types/util.ts: -------------------------------------------------------------------------------- 1 | import { Action, functionCall } from "near-api-js/lib/transaction"; 2 | import { FunctionCallAction } from "./interfaces"; 3 | 4 | /** 5 | * Converts a FunctionCallTransaction to an array of Action. 6 | * 7 | * @typeParam T - The type of the function call action arguments 8 | * @param action - The function call transaction to convert 9 | * @returns An array of Action objects 10 | */ 11 | export function convertToAction(action: FunctionCallAction): Action { 12 | return functionCall( 13 | action.params.methodName, 14 | convertToCompatibleFormat(action.params.args), 15 | BigInt(action.params.gas), 16 | BigInt(action.params.deposit) 17 | ); 18 | } 19 | 20 | /** 21 | * Converts a structure `T` into `object | Uint8Array` 22 | * 23 | * @typeParam T - The type of the input structure 24 | * @param input - The input structure to convert 25 | * @returns The converted result as either an object or Uint8Array 26 | * @throws Error if conversion fails 27 | */ 28 | export function convertToCompatibleFormat(input: T): object | Uint8Array { 29 | try { 30 | // Check if the input is already an object 31 | if (typeof input === "object" && input !== null) { 32 | return input; // Return the object as is 33 | } 34 | 35 | // Serialize to JSON and then to a Uint8Array 36 | const jsonString = JSON.stringify(input); 37 | return new TextEncoder().encode(jsonString); 38 | } catch (error: unknown) { 39 | const message = error instanceof Error ? error.message : String(error); 40 | throw new Error(`Failed to convert the input: ${message}`); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/mpcContract.test.ts: -------------------------------------------------------------------------------- 1 | import { createNearAccount, MpcContract } from "../../src/"; 2 | 3 | const TESTNET_CONFIG = { 4 | networkId: "testnet", 5 | nodeUrl: "https://rpc.testnet.near.org", 6 | }; 7 | const path = "derivationPath"; 8 | describe("mpcContract", () => { 9 | it("Contract (Read) Methods", async () => { 10 | const accountId = "farmface.testnet"; 11 | const contractId = "v1.signer-prod.testnet"; 12 | const account = await createNearAccount(accountId, TESTNET_CONFIG); 13 | const mpc = new MpcContract(account, contractId); 14 | const ethAddress = await mpc.deriveEthAddress(path); 15 | expect(mpc.accountId()).toEqual(contractId); 16 | expect(ethAddress).toEqual("0xaca49dcd616e2c1dce4e3490b49474af271790b5"); 17 | 18 | const signArgs = { 19 | payload: [1, 2], 20 | path, 21 | key_version: 0, 22 | }; 23 | const expected = { 24 | signerId: accountId, 25 | receiverId: contractId, 26 | actions: [ 27 | { 28 | type: "FunctionCall", 29 | params: { 30 | methodName: "sign", 31 | args: { 32 | request: { 33 | payload: [1, 2], 34 | path: "derivationPath", 35 | key_version: 0, 36 | }, 37 | }, 38 | gas: "250000000000000", 39 | deposit: "1", 40 | }, 41 | }, 42 | ], 43 | }; 44 | const result = await mpc.encodeSignatureRequestTx(signArgs); 45 | // The deposit is non-deterministic! 46 | expected.actions[0]!.params.deposit = result.actions[0]!.params.deposit; 47 | expect(result).toEqual(expected); 48 | // Set Gas: 49 | expected.actions[0]!.params.gas = "1"; 50 | expect(await mpc.encodeSignatureRequestTx(signArgs, 1n)).toEqual(expected); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "near-ca", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "description": "An SDK for controlling Ethereum Accounts from a Near Account.", 6 | "author": "@bh2smith", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/BitteProtocol/near-ca" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/BitteProtocol/near-ca/issues" 13 | }, 14 | "keywords": [ 15 | "near", 16 | "ethereum", 17 | "chain-signatures" 18 | ], 19 | "main": "dist/cjs/index.js", 20 | "module": "dist/esm/index.js", 21 | "types": "dist/esm/index.d.ts", 22 | "files": [ 23 | "dist/**/*" 24 | ], 25 | "scripts": { 26 | "build": "rm -fr dist/* && bun build:esm && bun build:cjs", 27 | "build:esm": "tsc -p tsconfig.esm.json", 28 | "build:cjs": "tsc -p tsconfig.cjs.json", 29 | "lint": "eslint . --ignore-pattern dist/ && prettier --check **/*.ts", 30 | "test": "jest --passWithNoTests", 31 | "coverage": "bun test --coverage", 32 | "verify": "bun coverage unit", 33 | "fmt": "prettier --write '{src,examples,tests}/**/*.{js,jsx,ts,tsx}' && eslint src/ --fix" 34 | }, 35 | "engines": { 36 | "node": ">=20.0.0" 37 | }, 38 | "dependencies": { 39 | "elliptic": "^6.6.1", 40 | "js-sha3": "^0.9.3", 41 | "near-api-js": "^6.4.0", 42 | "viem": "^2.38.6" 43 | }, 44 | "devDependencies": { 45 | "@types/elliptic": "^6.4.18", 46 | "@types/jest": "^30.0.0", 47 | "@types/node": "^24.9.2", 48 | "@typescript-eslint/eslint-plugin": "^8.46.2", 49 | "@typescript-eslint/parser": "^8.46.2", 50 | "dotenv": "^17.2.3", 51 | "eslint": "^9.39.0", 52 | "ethers": "^6.15.0", 53 | "opensea-js": "^7.4.0", 54 | "prettier": "^3.6.2", 55 | "ts-jest": "^29.4.5", 56 | "tsx": "^4.20.6", 57 | "typescript": "^5.9.3" 58 | }, 59 | "overrides": { 60 | "@noble/curves": "^1.9.7" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/unit/index.test.ts: -------------------------------------------------------------------------------- 1 | import { configFromNetworkId, setupAdapter } from "../../src/"; 2 | 3 | const mpcContractId = "v1.signer-prod.testnet"; 4 | const derivationPath = "ethereum,1"; 5 | 6 | describe("index", () => { 7 | it("setupAdapter", async () => { 8 | const adapter = await setupAdapter({ 9 | accountId: "your-account.testnet", 10 | network: { 11 | networkId: "testnet", 12 | nodeUrl: "https://rpc.testnet.near.org", 13 | }, 14 | mpcContractId, 15 | derivationPath, 16 | }); 17 | expect(adapter.address).toBe("0x5898502fc8577c5a0ae0c6984bb33c394c11a0a5"); 18 | }); 19 | 20 | it("setupAdapter with private Key", async () => { 21 | const adapter = await setupAdapter({ 22 | accountId: "your-account.testnet", 23 | network: { 24 | networkId: "testnet", 25 | nodeUrl: "https://rpc.testnet.near.org", 26 | }, 27 | mpcContractId, 28 | derivationPath, 29 | privateKey: 30 | "ed25519:3UEFmgr6SdPJYekHgQgaLjbHeqHnJ5FmpdQ6NxD2u1618y3hom7KrDxFEZJixYGg9XBxtwrs4hxb2ChYBMf2bCMp", 31 | }); 32 | expect(adapter.address).toBe("0x5898502fc8577c5a0ae0c6984bb33c394c11a0a5"); 33 | }); 34 | 35 | it("setupAdapter fails", async () => { 36 | const accountId = "your-account.testnet"; 37 | const networkId = "mainnet"; 38 | const config = { 39 | accountId, 40 | network: configFromNetworkId(networkId), 41 | mpcContractId, 42 | }; 43 | await expect(setupAdapter(config)).rejects.toThrow( 44 | `accountId ${accountId} doesn't match the networkId ${networkId}. Please ensure that your accountId is correct and corresponds to the intended network.` 45 | ); 46 | }); 47 | 48 | it("configFromNetworkId", async () => { 49 | expect(configFromNetworkId("mainnet")).toStrictEqual({ 50 | networkId: "mainnet", 51 | nodeUrl: "https://rpc.mainnet.near.org", 52 | }); 53 | 54 | expect(configFromNetworkId("testnet")).toStrictEqual({ 55 | networkId: "testnet", 56 | nodeUrl: "https://rpc.testnet.near.org", 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { Account, KeyPair } from "near-api-js"; 2 | import { MpcContract } from "./mpcContract"; 3 | import { 4 | configFromNetworkId, 5 | createNearAccount, 6 | getNetworkId, 7 | NearEthAdapter, 8 | } from "./chains"; 9 | import { isKeyPairString, SetupConfig } from "./types"; 10 | 11 | /** 12 | * Sets up the NEAR-Ethereum adapter using the provided configuration 13 | * 14 | * This function establishes a connection to the NEAR network using the given 15 | * account details, configures the Multi-Party Computation (MPC) contract, and 16 | * returns an instance of the NearEthAdapter. 17 | * 18 | * @param args - The configuration parameters for setting up the adapter 19 | * @returns An instance of NearEthAdapter configured with the provided settings 20 | * @throws Error if the `accountId` does not match the networkId of the provided or inferred `network` 21 | * @throws Error if there is a failure in creating a NEAR account 22 | */ 23 | export async function setupAdapter(args: SetupConfig): Promise { 24 | const { 25 | accountId, 26 | privateKey, 27 | mpcContractId, 28 | derivationPath = "ethereum,1", 29 | } = args; 30 | // Load near config from provided accountId if not provided 31 | const accountNetwork = getNetworkId(accountId); 32 | const config = args.network ?? configFromNetworkId(accountNetwork); 33 | if (accountNetwork !== config.networkId) { 34 | throw new Error( 35 | `accountId ${accountId} doesn't match the networkId ${config.networkId}. Please ensure that your accountId is correct and corresponds to the intended network.` 36 | ); 37 | } 38 | 39 | let account: Account; 40 | try { 41 | account = await createNearAccount( 42 | accountId, 43 | config, 44 | // Without private key, MPC contract connection is read-only. 45 | privateKey && isKeyPairString(privateKey) 46 | ? KeyPair.fromString(privateKey) 47 | : undefined 48 | ); 49 | } catch (error: unknown) { 50 | console.error(`Failed to create NEAR account: ${error}`); 51 | throw error; 52 | } 53 | return NearEthAdapter.fromConfig({ 54 | mpcContract: new MpcContract(account, mpcContractId, args.rootPublicKey), 55 | derivationPath: derivationPath, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | You can run any of the following example scripts using the command: 4 | 5 | ```bash 6 | npx tsx examples/*.ts 7 | ``` 8 | 9 | ### NEAR Credentials 10 | 11 | Before using NEAR-CA, ensure you have the following environment variables set in your `.env` file: 12 | 13 | - `NEAR_ACCOUNT_ID`: Your NEAR account identifier. 14 | - `MPC_CONTRACT_ID`: The NEAR contract that handles multichain operations. 15 | - `NETWORK`: Either `near` or `testnet`. 16 | - `NEAR_ACCOUNT_PRIVATE_KEY`: Your NEAR account private key. 17 | 18 | Copy the `.env.example` file and add these values to the `.env` file. 19 | 20 | For setting up a wallet, use the NEAR testnet wallet. 21 | The testnet wallet is different from the main wallet. 22 | For example, you can use the [Bitte Wallet](https://testnet.wallet.bitte.ai/). 23 | 24 | ## Fund your account 25 | 26 | Get your address 27 | 28 | ```sh 29 | npx tsx examples/getEthAddress.ts 30 | ``` 31 | 32 | After getting your address fund it from one of your own wallets. 33 | 34 | Here are some of the available examples: 35 | 36 | 1. **Basic:** 37 | - [Send ETH](./send-eth.ts) 38 | 2. **WETH Operations:** 39 | - [Deposit (Wrap-ETH)](./weth/wrap.ts) 40 | - [Withdraw (Unwrap-ETH)](./weth/unwrap.ts) 41 | 3. **NFT Operations:** 42 | - [Transfer ERC721](./nft/erc721/transfer.ts) 43 | 4. **Advanced:** 44 | - [Buy NFT on OpenSea](./opensea.ts) 45 | 46 | ## Example: Buy NFT by Collection Slug 47 | 48 | To buy an NFT using a collection slug, follow these steps: 49 | 50 | ```sh 51 | # Install dependencies 52 | yarn 53 | 54 | # Set up credentials 55 | cp .env.example .env # Paste your NEAR credentials into the .env file 56 | 57 | # Run the OpenSea example script 58 | npx tsx examples/opensea.ts 59 | ``` 60 | 61 | You will be prompted to provide a `collectionSlug`. 62 | 63 | ### What is a Collection Slug? 64 | 65 | A collection slug identifies a specific collection on OpenSea. To find a collection slug: 66 | 67 | 1. Visit [testnet.opensea](https://testnets.opensea.io/). 68 | 2. Browse and find a collection you like. 69 | 3. Copy the slug from the URL: `https://testnets.opensea.io/collection/[slug]`. 70 | 71 | For example, if the URL is `https://testnets.opensea.io/collection/the-monkey-chainz`, the collection slug is `the-monkey-chainz`. 72 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hex, 3 | fromHex, 4 | hashMessage, 5 | hashTypedData, 6 | isHex, 7 | keccak256, 8 | serializeTransaction, 9 | } from "viem"; 10 | import { populateTx } from "./transaction"; 11 | import { 12 | EncodedSignRequest, 13 | EthSignParams, 14 | EthTransactionParams, 15 | PersonalSignParams, 16 | SignRequestData, 17 | TypedDataParams, 18 | } from "../types"; 19 | 20 | /** 21 | * Routes signature requests to appropriate handlers based on method type 22 | * 23 | * @param request - The signature request data 24 | * @returns Object containing the EVM message, payload hash, and recovery data 25 | */ 26 | export async function requestRouter( 27 | request: SignRequestData 28 | ): Promise { 29 | const { method, chainId, params } = request; 30 | switch (method) { 31 | case "eth_sign": { 32 | const [_, messageHash] = params as EthSignParams; 33 | return { 34 | evmMessage: fromHex(messageHash, "string"), 35 | hashToSign: hashMessage({ raw: messageHash }), 36 | }; 37 | } 38 | case "personal_sign": { 39 | const [messageHash, _] = params as PersonalSignParams; 40 | return { 41 | evmMessage: fromHex(messageHash, "string"), 42 | hashToSign: hashMessage({ raw: messageHash }), 43 | }; 44 | } 45 | case "eth_sendTransaction": { 46 | // We only support one transaction at a time! 47 | let rlpTx: Hex; 48 | if (isHex(params)) { 49 | rlpTx = params; 50 | } else { 51 | const tx = params[0] as EthTransactionParams; 52 | const transaction = await populateTx( 53 | { 54 | to: tx.to, 55 | chainId, 56 | value: fromHex(tx.value || "0x0", "bigint"), 57 | data: tx.data || "0x", 58 | ...(tx.gas ? { gas: fromHex(tx.gas, "bigint") } : {}), 59 | }, 60 | tx.from 61 | ); 62 | rlpTx = serializeTransaction(transaction); 63 | } 64 | 65 | return { 66 | hashToSign: keccak256(rlpTx), 67 | evmMessage: rlpTx, 68 | }; 69 | } 70 | case "eth_signTypedData": 71 | case "eth_signTypedData_v4": { 72 | const [_, dataString] = params as TypedDataParams; 73 | const typedData = JSON.parse(dataString); 74 | return { 75 | evmMessage: dataString, 76 | hashToSign: hashTypedData(typedData), 77 | }; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/utils/kdf.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | najPublicKeyStrToUncompressedHexPoint, 3 | deriveChildPublicKey, 4 | uncompressedHexPointToEvmAddress, 5 | } from "../../../src/utils/kdf"; 6 | 7 | const ROOT_PK = 8 | "secp256k1:54hU5wcCmVUPFWLDALXMh1fFToZsVXrx9BbTbHzSfQq1Kd1rJZi52iPa4QQxo6s5TgjWqgpY8HamYuUDzG6fAaUq"; 9 | const DECOMPRESSED_HEX = 10 | "04cb41bab8bc97121f4902514ca57a284f167b9239ecb8176831d1ef0fede87c61ca3e59da1c194aa90108098a9e5cdc55d3b3297cdefbc085ffafd0f2c34ae61a"; 11 | const CHILD_PK = 12 | "0430dbf32f29a5e9d8df4173d940932be30baf28bae98bf93476eba7d5e2c3d838e807f6b7442e7796e2ea1550a6c1f6de1367a7cdb6f0e68a1e36fafd35eb6ea0"; 13 | const CHILD_PK_NO_PATH = 14 | "0470649ea5975a2bdb5895c78d8da22e0fb8ddc5099fde4d8c826107bd9a705ad3d61644d844704dfd6467a46e6e2b19e61568c7909c4966f0577286b72447f420"; 15 | 16 | describe("Crypto Functions", () => { 17 | it("converts NEAR public key string to uncompressed hex point", () => { 18 | const result = najPublicKeyStrToUncompressedHexPoint(ROOT_PK); 19 | expect(result).toMatch(/^04[0-9a-f]+$/); 20 | expect(result).toEqual(DECOMPRESSED_HEX); 21 | }); 22 | 23 | it("derives child public key", async () => { 24 | const parentHex = DECOMPRESSED_HEX; 25 | const signerId = "neareth-dev.testnet"; 26 | const path = "ethereum,1"; 27 | const result = await deriveChildPublicKey(parentHex, signerId, path); 28 | expect(result).toMatch(/^04[0-9a-f]+$/); 29 | expect(result).toEqual(CHILD_PK); 30 | }); 31 | 32 | it("derives child public key without path", async () => { 33 | const parentHex = DECOMPRESSED_HEX; 34 | const signerId = "ethdenver2024.testnet"; 35 | const result = await deriveChildPublicKey(parentHex, signerId); 36 | expect(result).toMatch(/^04[0-9a-f]+$/); 37 | expect(result).toEqual(CHILD_PK_NO_PATH); 38 | }); 39 | 40 | it("new address from near", async () => { 41 | const parentHex = DECOMPRESSED_HEX; 42 | const signerId = "neareth-dev.testnet"; 43 | const path = "ethereum,1"; 44 | const publicKey = await deriveChildPublicKey(parentHex, signerId, path); 45 | expect(uncompressedHexPointToEvmAddress(publicKey)).toEqual( 46 | "0x759e10411dda5138e331b7ad5ce1b937550db737" 47 | ); 48 | }); 49 | 50 | it("converts uncompressed hex point to EVM address", () => { 51 | const result = uncompressedHexPointToEvmAddress(CHILD_PK); 52 | expect(result).toMatch(/^0x[0-9a-fA-F]{40}$/); 53 | expect(result).toMatch("0x759e10411dda5138e331b7ad5ce1b937550db737"); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/utils/kdf.ts: -------------------------------------------------------------------------------- 1 | import { base_decode } from "near-api-js/lib/utils/serialize"; 2 | import { ec as EC } from "elliptic"; 3 | import { Address, keccak256 } from "viem"; 4 | import { sha3_256 } from "js-sha3"; 5 | 6 | /** 7 | * Converts a NEAR account public key string to an uncompressed hex point 8 | * 9 | * @param najPublicKeyStr - The NEAR account public key string 10 | * @returns Uncompressed hex point string prefixed with "04" 11 | */ 12 | export function najPublicKeyStrToUncompressedHexPoint( 13 | najPublicKeyStr: string 14 | ): string { 15 | const decodedKey = base_decode(najPublicKeyStr.split(":")[1]!); 16 | return "04" + Buffer.from(decodedKey).toString("hex"); 17 | } 18 | 19 | /** 20 | * Derives a child public key using elliptic curve operations 21 | * 22 | * @param parentUncompressedPublicKeyHex - Parent public key as uncompressed hex 23 | * @param signerId - The signer's identifier 24 | * @param path - Optional derivation path (defaults to empty string) 25 | * @returns Derived child public key as uncompressed hex string 26 | */ 27 | export function deriveChildPublicKey( 28 | parentUncompressedPublicKeyHex: string, 29 | signerId: string, 30 | path: string = "" 31 | ): string { 32 | const ec = new EC("secp256k1"); 33 | const scalarHex = sha3_256( 34 | `near-mpc-recovery v0.1.0 epsilon derivation:${signerId},${path}` 35 | ); 36 | 37 | const x = parentUncompressedPublicKeyHex.substring(2, 66); 38 | const y = parentUncompressedPublicKeyHex.substring(66); 39 | 40 | // Create a point object from X and Y coordinates 41 | const oldPublicKeyPoint = ec.curve.point(x, y); 42 | 43 | // Multiply the scalar by the generator point G 44 | const scalarTimesG = ec.g.mul(scalarHex); 45 | 46 | // Add the result to the old public key point 47 | const newPublicKeyPoint = oldPublicKeyPoint.add(scalarTimesG); 48 | const newX = newPublicKeyPoint.getX().toString("hex").padStart(64, "0"); 49 | const newY = newPublicKeyPoint.getY().toString("hex").padStart(64, "0"); 50 | return "04" + newX + newY; 51 | } 52 | 53 | /** 54 | * Converts an uncompressed hex point to an Ethereum address 55 | * 56 | * @param uncompressedHexPoint - The uncompressed hex point string 57 | * @returns Ethereum address derived from the public key 58 | * @remarks Takes the last 20 bytes of the keccak256 hash of the public key 59 | */ 60 | export function uncompressedHexPointToEvmAddress( 61 | uncompressedHexPoint: string 62 | ): Address { 63 | const addressHash = keccak256(`0x${uncompressedHexPoint.slice(2)}`); 64 | // Ethereum address is last 20 bytes of hash (40 characters), prefixed with 0x 65 | return ("0x" + addressHash.substring(addressHash.length - 40)) as Address; 66 | } 67 | -------------------------------------------------------------------------------- /tests/unit/utils/transaction.test.ts: -------------------------------------------------------------------------------- 1 | import { zeroAddress } from "viem"; 2 | import { Network, TransactionWithSignature } from "../../../src"; 3 | import { 4 | buildTxPayload, 5 | addSignature, 6 | toPayload, 7 | populateTx, 8 | fromPayload, 9 | } from "../../../src/utils/transaction"; 10 | 11 | describe("Transaction Builder Functions", () => { 12 | it("buildTxPayload", async () => { 13 | const txHash = 14 | "0x02e783aa36a7808309e8bb84773f7cbb8094deadbeef0000000000000000000000000b00b1e50180c0"; 15 | const payload = buildTxPayload(txHash); 16 | expect(payload).toEqual([ 17 | 179, 145, 88, 233, 130, 68, 7, 211, 98, 140, 132, 230, 199, 101, 9, 36, 18 | 37, 94, 214, 13, 217, 70, 225, 215, 212, 59, 210, 203, 239, 90, 243, 178, 19 | ]); 20 | }); 21 | 22 | it("pass: toPayload", async () => { 23 | const txHash = 24 | "0x52b6437db56d87f5991d7c173cf11b9dd0f9fb083260bef1bf0c338042bc398c"; 25 | expect(toPayload(txHash)).toStrictEqual([ 26 | 82, 182, 67, 125, 181, 109, 135, 245, 153, 29, 124, 23, 60, 241, 27, 157, 27 | 208, 249, 251, 8, 50, 96, 190, 241, 191, 12, 51, 128, 66, 188, 57, 140, 28 | ]); 29 | }); 30 | 31 | it("fails: toPayload", async () => { 32 | const txHash = 33 | "0x02e783aa36a7808309e8bb84773f7cbb8094deadbeef0000000000000000000000000b00b1e50180c00"; 34 | expect(() => toPayload(txHash)).toThrow( 35 | `Payload must have 32 bytes: ${txHash}` 36 | ); 37 | }); 38 | 39 | it("pass: fromPayload", async () => { 40 | const txHash = 41 | "0x52b6437db56d87f5991d7c173cf11b9dd0f9fb083260bef1bf0c338042bc398c"; 42 | expect(fromPayload(toPayload(txHash))).toBe(txHash); 43 | }); 44 | it("addSignature", async () => { 45 | const testTx: TransactionWithSignature = { 46 | transaction: 47 | "0x02e883aa36a780845974e6f084d0aa7af08094deadbeef0000000000000000000000000b00b1e50180c0", 48 | signature: { 49 | r: "0xEF532579E267C932B959A1ADB9E455AC3C5397D0473471C4C3DD5D62FD4D7EDE", 50 | s: "0x7C195E658C713D601D245311A259115BB91EC87C86ACB07C03BD9C1936A6A9E8", 51 | yParity: 1, 52 | }, 53 | }; 54 | expect(addSignature(testTx)).toEqual( 55 | "0x02f86b83aa36a780845974e6f084d0aa7af08094deadbeef0000000000000000000000000b00b1e50180c001a0ef532579e267c932b959a1adb9e455ac3c5397d0473471c4c3dd5d62fd4d7edea07c195e658c713d601d245311a259115bb91ec87c86acb07c03bd9c1936a6a9e8" 56 | ); 57 | }); 58 | 59 | it("populateTx", async () => { 60 | const baseTx = { 61 | chainId: 11155111, 62 | to: zeroAddress, 63 | }; 64 | await expect( 65 | populateTx(baseTx, zeroAddress, Network.fromChainId(100).client) 66 | ).rejects.toThrow("client chainId=100 mismatch with tx.chainId=11155111"); 67 | 68 | const tx = await populateTx(baseTx, zeroAddress); 69 | expect(tx.to).toEqual(zeroAddress); 70 | expect(tx.value).toEqual(0n); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/chains/near.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair, Account, KeyPairSigner } from "near-api-js"; 2 | import { JsonRpcProvider, Provider } from "near-api-js/lib/providers"; 3 | import { NearConfig } from "near-api-js/lib/near"; 4 | import { NearAccountConfig } from "../types"; 5 | 6 | /** Gas unit constant for NEAR transactions (1 TeraGas) */ 7 | export const TGAS = 1000000000000n; 8 | 9 | /** Valid NEAR network identifiers */ 10 | type NetworkId = "mainnet" | "testnet"; 11 | 12 | /** 13 | * Extracts the network ID from a given NEAR account ID 14 | * 15 | * @param accountId - The NEAR account ID to analyze 16 | * @returns The network ID ("mainnet" or "testnet") 17 | * @remarks If the account ID doesn't end with "near" or "testnet", defaults to "mainnet" 18 | */ 19 | export function getNetworkId(accountId: string): NetworkId { 20 | const accountExt = accountId.split(".").pop() || ""; 21 | // Consider anything that isn't testnet as mainnet. 22 | return accountExt !== "testnet" ? "mainnet" : accountExt; 23 | } 24 | 25 | /** 26 | * Generates a NEAR configuration object for a specific network 27 | * 28 | * @param networkId - The target network identifier 29 | * @returns Configuration object for NEAR connection 30 | */ 31 | export function configFromNetworkId(networkId: NetworkId): NearConfig { 32 | return { 33 | networkId, 34 | nodeUrl: `https://rpc.${networkId}.near.org`, 35 | }; 36 | } 37 | 38 | /** 39 | * Creates a NEAR Account instance from provided credentials 40 | * 41 | * @param config - Configuration containing account ID, network, and key pair 42 | * @returns A NEAR Account instance 43 | */ 44 | export const nearAccountFromKeyPair = async ( 45 | config: NearAccountConfig 46 | ): Promise => { 47 | return createNearAccount(config.accountId, config.network, config.keyPair); 48 | }; 49 | 50 | /** 51 | * Creates a read-only NEAR Account instance from an account ID 52 | * 53 | * @param accountId - The NEAR account identifier 54 | * @param network - The NEAR network configuration 55 | * @returns A read-only NEAR Account instance 56 | * @remarks This account cannot perform write operations 57 | */ 58 | export const nearAccountFromAccountId = async ( 59 | accountId: string, 60 | network: NearConfig 61 | ): Promise => { 62 | return createNearAccount(accountId, network); 63 | }; 64 | 65 | /** 66 | * Creates a NEAR Account instance with optional write capabilities 67 | * 68 | * @param accountId - The NEAR account identifier 69 | * @param network - The NEAR network configuration 70 | * @param keyPair - Optional key pair for write access 71 | * @returns A NEAR Account instance 72 | */ 73 | export const createNearAccount = async ( 74 | accountId: string, 75 | network: NearConfig, 76 | keyPair?: KeyPair 77 | ): Promise => { 78 | const provider = new JsonRpcProvider({ url: network.nodeUrl }) as Provider; 79 | return new Account( 80 | accountId, 81 | provider, 82 | keyPair ? KeyPairSigner.fromSecretKey(keyPair.toString()) : undefined 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /examples/opensea.ts: -------------------------------------------------------------------------------- 1 | import { OpenSeaSDK, Chain, OrderSide } from "opensea-js"; 2 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter, sleep } from "./setup"; 3 | import * as readline from "readline"; 4 | import { ethers } from "ethers"; 5 | import { Address, Hex, encodeFunctionData } from "viem"; 6 | import seaportABI from "./abis/Seaport.json"; 7 | 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | // This script uses the OpenSea SDK: 14 | // https://github.com/ProjectOpenSea/opensea-js/blob/main/developerDocs/advanced-use-cases.md 15 | const run = async (slug: string): Promise => { 16 | const evm = await setupNearEthAdapter(); 17 | // This fake provider is required to construct an openseaSDK instance (although we do not make use of it). 18 | const dummyProvider = new ethers.JsonRpcProvider("fakeURL", 11155111); 19 | const openseaSDK = new OpenSeaSDK(dummyProvider, { 20 | chain: Chain.Sepolia, 21 | // apiKey: YOUR_API_KEY, 22 | }); 23 | // const slug = "mintbase-chain-abstraction-v2"; 24 | 25 | console.log("Retrieving Listings for..."); 26 | const listings = (await openseaSDK.api.getAllListings(slug)).listings; 27 | if (listings.length === 0) { 28 | console.log(`No available listings for collection: ${slug}`); 29 | return; 30 | } 31 | listings.sort((a, b) => 32 | a.price.current.value.localeCompare(b.price.current.value) 33 | ); 34 | const cheapestAvailable = listings[0]; 35 | console.log( 36 | `Got ${listings.length} Listings, purchasing the cheapest available` 37 | ); 38 | 39 | // This sleep is due to free-tier testnet rate limiting. 40 | await sleep(1000); 41 | const data = await openseaSDK.api.generateFulfillmentData( 42 | evm.address, 43 | cheapestAvailable.order_hash, 44 | cheapestAvailable.protocol_address, 45 | OrderSide.ASK 46 | ); 47 | 48 | const tx = data.fulfillment_data.transaction; 49 | const input_data = tx.input_data; 50 | 51 | // TODO - report or fix these bugs with OpenseaSDK 52 | // @ts-expect-error: Undocumented field on type FulfillmentData within FulfillmentDataResponse 53 | const order = input_data.parameters; 54 | // @ts-expect-error: Undocumented field on type FulfillmentData within FulfillmentDataResponse 55 | const fulfillerConduitKey = input_data.fulfillerConduitKey; 56 | 57 | let callData = "0x"; 58 | if (tx.function.includes("fulfillOrder")) { 59 | console.log("Using fulfillOrder"); 60 | callData = encodeFunctionData({ 61 | abi: seaportABI, 62 | functionName: "fulfillOrder", 63 | args: [order, fulfillerConduitKey], 64 | }); 65 | } else { 66 | console.log("Using fulfillBasicOrder_efficient_6GL6yc"); 67 | callData = encodeFunctionData({ 68 | abi: seaportABI, 69 | functionName: "fulfillBasicOrder_efficient_6GL6yc", 70 | args: [order], 71 | }); 72 | } 73 | await evm.signAndSendTransaction({ 74 | to: tx.to as Address, 75 | value: BigInt(tx.value), 76 | data: callData as Hex, 77 | chainId: SEPOLIA_CHAIN_ID, 78 | }); 79 | }; 80 | 81 | rl.question("Provide collection slug: ", (input) => { 82 | run(input); 83 | }); 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "moduleResolution": "Node", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | 8 | // Best practices 9 | "strict": true, /* Enable all strict type-checking options. */ 10 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 11 | "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 12 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 13 | "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 14 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 15 | "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 16 | "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 17 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 18 | "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 19 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 20 | "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 21 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 22 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 23 | "noUncheckedIndexedAccess": true, 24 | "noPropertyAccessFromIndexSignature": false, // This is actually loose. 25 | "skipLibCheck": true, 26 | 27 | // New Strict Settings 28 | "allowUnreachableCode": false, /* Error on unreachable code */ 29 | "allowUnusedLabels": false, /* Error on unused labels */ 30 | "noImplicitOverride": true, /* Ensure overriding members are marked with override modifier */ 31 | "noEmitOnError": true, /* Do not emit outputs if any errors were reported */ 32 | "isolatedModules": true, /* Ensure each file can be safely transpiled without relying on other imports */ 33 | "forceConsistentCasingInFileNames": true, /* Ensure consistent casing in file names */ 34 | // TODO(maybe): change all type imports to explicitly say import type X from ... 35 | // "verbatimModuleSyntax": true, /* Only allow import/export statements that are part of ES modules */ 36 | }, 37 | "include": ["src/**/*.ts"], 38 | "exclude": ["node_modules", "dist", "examples", "tests", "eslint.config.js"] 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/mock-sign.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hex, Signature } from "viem"; 2 | import { PrivateKeyAccount, privateKeyToAccount } from "viem/accounts"; 3 | import { FunctionCallTransaction, SignArgs } from "../types"; 4 | import { Account } from "near-api-js"; 5 | import { 6 | fromPayload, 7 | IMpcContract, 8 | nearAccountFromAccountId, 9 | NearEthAdapter, 10 | } from ".."; 11 | 12 | /** 13 | * Converts a raw hexadecimal signature into a structured Signature object 14 | * 15 | * @param hexSignature - The raw hexadecimal signature (e.g., '0x...') 16 | * @returns A structured Signature object with fields r, s, v, and yParity 17 | * @throws Error if signature length is invalid 18 | */ 19 | function hexToSignature(hexSignature: Hex): Signature { 20 | const cleanedHex = hexSignature.slice(2); 21 | 22 | if (cleanedHex.length !== 130) { 23 | throw new Error( 24 | `Invalid hex signature length: ${cleanedHex.length}. Expected 130 characters (65 bytes).` 25 | ); 26 | } 27 | 28 | const v = BigInt(`0x${cleanedHex.slice(128, 130)}`); 29 | return { 30 | r: `0x${cleanedHex.slice(0, 64)}`, 31 | s: `0x${cleanedHex.slice(64, 128)}`, 32 | v, 33 | yParity: v === 27n ? 0 : v === 28n ? 1 : undefined, 34 | }; 35 | } 36 | 37 | /** Mock implementation of the MPC Contract interface for testing */ 38 | export class MockMpcContract implements IMpcContract { 39 | connectedAccount: Account; 40 | private ethAccount: PrivateKeyAccount; 41 | 42 | /** 43 | * Creates a new mock MPC contract instance 44 | * 45 | * @param account - The NEAR account to use 46 | * @param privateKey - Optional private key (defaults to deterministic test key) 47 | */ 48 | constructor(account: Account, privateKey?: Hex) { 49 | this.connectedAccount = account; 50 | this.ethAccount = privateKeyToAccount( 51 | privateKey || 52 | "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 53 | ); 54 | } 55 | 56 | /** Gets the mock contract ID */ 57 | accountId(): string { 58 | return "mock-mpc.offline"; 59 | } 60 | 61 | /** 62 | * Returns the mock Ethereum address 63 | * 64 | * @returns The Ethereum address associated with the private key 65 | */ 66 | deriveEthAddress = async (_unused?: string): Promise
=> { 67 | return this.ethAccount.address; 68 | }; 69 | 70 | /** 71 | * Signs a message using the mock private key 72 | * 73 | * @param signArgs - The signature request arguments 74 | * @returns The signature 75 | */ 76 | requestSignature = async (signArgs: SignArgs): Promise => { 77 | const hexSignature = await this.ethAccount.sign({ 78 | hash: fromPayload(signArgs.payload), 79 | }); 80 | return hexToSignature(hexSignature); 81 | }; 82 | 83 | /** 84 | * Encodes a mock signature request transaction 85 | * 86 | * @param signArgs - The signature request arguments 87 | * @param gas - Optional gas limit 88 | * @returns The encoded transaction 89 | */ 90 | async encodeSignatureRequestTx( 91 | signArgs: SignArgs, 92 | gas?: bigint 93 | ): Promise> { 94 | return { 95 | signerId: this.connectedAccount.accountId, 96 | receiverId: this.accountId(), 97 | actions: [ 98 | { 99 | type: "FunctionCall", 100 | params: { 101 | methodName: "sign", 102 | args: { request: signArgs }, 103 | gas: gas ? gas.toString() : "1", 104 | deposit: "1", 105 | }, 106 | }, 107 | ], 108 | }; 109 | } 110 | } 111 | 112 | /** 113 | * Creates a mock adapter instance for testing 114 | * 115 | * @param privateKey - Optional private key for the mock contract 116 | * @returns A configured NearEthAdapter instance 117 | */ 118 | export async function mockAdapter(privateKey?: Hex): Promise { 119 | const account = await nearAccountFromAccountId("mock-user.offline", { 120 | networkId: "testnet", 121 | nodeUrl: "https://rpc.testnet.near.org", 122 | }); 123 | const mpcContract = new MockMpcContract(account, privateKey); 124 | return NearEthAdapter.fromConfig({ mpcContract }); 125 | } 126 | -------------------------------------------------------------------------------- /tests/unit/utils/signature.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | signatureFromOutcome, 3 | signatureFromTxHash, 4 | transformSignature, 5 | } from "../../../src/utils/signature"; 6 | 7 | describe("utility: get Signature", () => { 8 | const url: string = "https://archival-rpc.testnet.near.org"; 9 | const accountId = "neareth-dev.testnet"; 10 | const successHash = "CYGDarJXUtUug83ur6QsRzr86bDjAxN3wk8N3acGXMgg"; 11 | const relayedSuccessHash = "FWtVVNGLkdAmHwQQCHvZCbNGynEWSJbeKA5GCZy1ghYf"; 12 | const failedHash = "ERdVFTNmuf1uHsiGyTu2n6XDbVfXqZXQ4N9rU6BqRMjk"; 13 | const nonExistantTxHash = "7yRm5FjHn9raRYPoHH6wimizhT53PnPnuvkpecyQDqLY"; 14 | const nonSignatureRequestHash = 15 | "BCxYwZ6YfscaHza5MEDmk4DRgKZWJ77ZtBS5L9kH3Ve7"; 16 | 17 | it("successful: signatureFromTxHash", async () => { 18 | const sig = await signatureFromTxHash(url, successHash, accountId); 19 | expect(sig).toEqual({ 20 | r: "0x200209319EBF0858BB8543A9A927BDE6A54E7BD4914B76F96BDF67AEA4211CDD", 21 | s: "0x412F478B129A7A586B158BA178C7A921978473384130ACF9E4034E16063FF5B5", 22 | yParity: 0, 23 | }); 24 | 25 | const relayedSig = await signatureFromTxHash( 26 | url, 27 | relayedSuccessHash, 28 | "mintbase.testnet" 29 | ); 30 | expect(relayedSig).toEqual({ 31 | r: "0xFEA01D93DFF2EAA73F81545902788603E8D930786B809DC9DC62E5680D91DD72", 32 | s: "0x4A4487EA25EDFEBBEE1FBA556AE4F90E141CAAFA13648CA8C8D144890F8EA1C4", 33 | yParity: 0, 34 | }); 35 | }); 36 | 37 | it("signatureFromTxHash fails Error", async () => { 38 | await expect(signatureFromTxHash(url, failedHash)).rejects.toThrow( 39 | `Signature Request Failed in ${failedHash}` 40 | ); 41 | }); 42 | 43 | it("signatureFromTxHash fails with no signature", async () => { 44 | await expect( 45 | signatureFromTxHash(url, nonSignatureRequestHash) 46 | ).rejects.toThrow( 47 | `No detectable signature found in transaction ${nonSignatureRequestHash}` 48 | ); 49 | }); 50 | 51 | // This one takes too long. 52 | it.skip("signatureFromTxHash fails with server error", async () => { 53 | await expect(signatureFromTxHash(url, nonExistantTxHash)).rejects.toThrow( 54 | "JSON-RPC error: Server error" 55 | ); 56 | }); 57 | 58 | it("signatureFromTxHash fails with parse error", async () => { 59 | await expect(signatureFromTxHash(url, "nonsense")).rejects.toThrow( 60 | "HTTP error! status: 400" 61 | ); 62 | }); 63 | 64 | it("transforms mpcSignature", () => { 65 | const signature = { 66 | big_r: { 67 | affine_point: 68 | "0337F110D095850FD1D6451B30AF40C15A82566C7FA28997D3EF83C5588FBAF99C", 69 | }, 70 | s: { 71 | scalar: 72 | "4C5D1C3A8CAFF5F0C13E34B4258D114BBEAB99D51AF31648482B7597F3AD5B72", 73 | }, 74 | recovery_id: 1, 75 | }; 76 | expect(transformSignature(signature)).toEqual({ 77 | r: "0x37F110D095850FD1D6451B30AF40C15A82566C7FA28997D3EF83C5588FBAF99C", 78 | s: "0x4C5D1C3A8CAFF5F0C13E34B4258D114BBEAB99D51AF31648482B7597F3AD5B72", 79 | yParity: 1, 80 | }); 81 | }); 82 | 83 | it("signatureForOutcome", () => { 84 | // Outcome from: wkBWmTXoUgdm7RvdwNTUmtmDZLB14TfXMdyf57UXAaZ 85 | const outcome = { 86 | status: { 87 | SuccessValue: 88 | "eyJiaWdfciI6eyJhZmZpbmVfcG9pbnQiOiIwMkVBRDJCMUYwN0NDNDk4REIyNTU2MzE0QTZGNzdERkUzRUUzRDE0NTNCNkQ3OTJBNzcwOTE5MjRFNTFENEMyNDcifSwicyI6eyJzY2FsYXIiOiIxQTlGNjBDMkNDMjM5OEE1MDk3N0Q0Q0E5M0M0MDE2OEU4RjJDRTdBOUM5MEUzNzQ1MjJERjNDNzZDRjU0RjJFIn0sInJlY292ZXJ5X2lkIjoxfQ==", 89 | }, 90 | // irrelevant fields 91 | receipts_outcome: [], 92 | transaction: null, 93 | transaction_outcome: { 94 | id: "", 95 | outcome: { 96 | logs: [], 97 | receipt_ids: [], 98 | gas_burnt: 0, 99 | tokens_burnt: "", 100 | executor_id: "string", 101 | status: {}, 102 | }, 103 | }, 104 | }; 105 | expect(signatureFromOutcome(outcome)).toEqual({ 106 | r: "0xEAD2B1F07CC498DB2556314A6F77DFE3EE3D1453B6D792A77091924E51D4C247", 107 | s: "0x1A9F60C2CC2398A50977D4CA93C40168E8F2CE7A9C90E374522DF3C76CF54F2E", 108 | yParity: 1, 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEAR Chain Abstraction Layer (NEAR-CA) 2 | 3 | NEAR-CA is a TypeScript library designed to provide an abstraction layer for interacting with the NEAR blockchain, simplifying the process of performing transactions and managing accounts on both NEAR and Ethereum chains. This library is intended for use in server-side applications only. 4 | 5 | ## Features 6 | 7 | - EVM Account Derivation from NEAR blockchain. 8 | - Transaction signing and sending on the Ethereum blockchain. 9 | - Key derivation functions for cryptographic operations. 10 | - Support for EIP-1559 transactions on Ethereum. 11 | - Wallet Connect intergration tools. 12 | 13 | ### Usage 14 | 15 | ## CLI 16 | 17 | For Ethereum, you can derive addresses, create payloads for transactions, and send signed transactions. 18 | 19 | For more detailed usage examples, see the [Examples README](./examples/README.md). 20 | 21 | ## Integrations 22 | 23 | [near-safe](https://github.com/BitteProtocol/near-safe) extends this tool kit by using the EOA as an owner of an ERC-4337 [Safe](https://safe.global/) account. 24 | 25 | ## Frontend/UI 26 | 27 | Install near-ca, run the following command: 28 | 29 | ```bash 30 | yarn add near-ca 31 | ``` 32 | 33 | ### Example: Setup NearEthAdapter and Send ETH 34 | 35 | Here's an example of how to set up the `NearEthAdapter` and send ETH: 36 | 37 | ```typescript 38 | import dotenv from "dotenv"; 39 | import { 40 | broadcastSignedTransaction, 41 | convertToAction, 42 | isRlpHex, 43 | setupAdapter, 44 | signatureFromOutcome, 45 | } from "near-ca"; 46 | 47 | dotenv.config(); 48 | const { NEAR_ACCOUNT_ID, NEAR_ACCOUNT_PRIVATE_KEY } = process.env; 49 | 50 | const adapter = await setupAdapter({ 51 | accountId: NEAR_ACCOUNT_ID!, 52 | mpcContractId: MPC_CONTRACT_ID!, 53 | // privateKey: NEAR_ACCOUNT_PRIVATE_KEY!, // Optional depending on setup 54 | }); 55 | 56 | const { 57 | evmMessage, 58 | nearPayload: { receiverId, actions }, 59 | } = await evm.encodeSignRequest({ 60 | method: "eth_sendTransaction", 61 | chainId: 11_155_111, // Sepolia 62 | params: [ 63 | { 64 | from: evm.address, 65 | to: "0xdeADBeeF0000000000000000000000000b00B1e5", 66 | value: "0x01", // 1 WEI 67 | // data: "0x", // Optional 68 | }, 69 | ], 70 | }); 71 | console.log(`Requesting Signature for ${evmMessage}`); 72 | // Using your near Account, send the nearPaylod as signature request: 73 | const nearAccount = adapter.nearAccount(); 74 | // 75 | const outtcome = await nearAccount.signAndSendTransaction({ 76 | receiverId, 77 | actions: actions.map((a) => convertToAction(a)), 78 | }); 79 | const signature = signatureFromOutcome(outtcome); 80 | console.log("Signature aquired!"); 81 | if (isRlpHex(evmMessage)) { 82 | // This will be true for what we built above. 83 | broadcastSignedTransaction({ transaction: evmMessage, signature }); 84 | } else { 85 | // Use Signature for whatever else. 86 | } 87 | ``` 88 | 89 | ### Other Examples (CLI) 90 | 91 | These examples require Private Key to be supplied: 92 | 93 | Each of the following scripts can be run with 94 | 95 | ```bash 96 | npx tsx examples/*.ts 97 | ``` 98 | 99 | 1. [(Basic) Send ETH](./examples/send-eth.ts) 100 | 2. **WETH** 101 | - [Deposit (aka wrap-ETH)](./examples/weth/wrap.ts) 102 | - [Withdraw (aka unwrap-ETH)](./examples/weth/wrap.ts) 103 | 3. [Transfer ERC721](./examples/nft/erc721/transfer.ts) 104 | 4. [(Advanced) Buy NFT On Opensea](./examples/opensea.ts) 105 | 106 | ## Configuration 107 | 108 | Before using NEAR-CA, ensure you have the following environment variables set in your `.env` file: 109 | 110 | - `NEAR_ACCOUNT_ID`: Your NEAR account identifier. 111 | - `NEAR_ACCOUNT_PRIVATE_KEY`: Your NEAR account private key. 112 | - `MPC_CONTRACT_ID`: The NEAR contract that handles multichain operations. 113 | - `NETWORK`: Either `near` or `testnet`. 114 | 115 | Copy the `.env.example` file and add these values to the `.env` file. 116 | 117 | Steps to get your `NEAR_ACCOUNT_ID` and `NEAR_ACCOUNT_PRIVATE_KEY`: 118 | 119 | 1. Create a Near wallet address, super easy, here: https://wallet.bitte.ai/ 120 | 2. Your `XYZ.near` is your `NEAR_ACCOUNT_ID`. 121 | 3. [Visit Settings Page](https://wallet.bitte.ai/settings) 122 | 4. Go to "Security & Recovery" -> "Export Account". 123 | 5. After the exporting is complete click on "Private Key" and copy it. 124 | 6. Paste it to `NEAR_ACCOUNT_PRIVATE_KEY` in your `.env` file. 125 | -------------------------------------------------------------------------------- /src/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hash, 3 | Hex, 4 | PublicClient, 5 | TransactionSerializable, 6 | isBytes, 7 | keccak256, 8 | parseTransaction, 9 | serializeTransaction, 10 | toBytes, 11 | toHex, 12 | } from "viem"; 13 | import { BaseTx, TransactionWithSignature } from "../types"; 14 | import { Network } from "../network"; 15 | 16 | /** 17 | * Converts a message hash to a payload array 18 | * 19 | * @param msgHash - The message hash to convert 20 | * @returns Array of numbers representing the payload 21 | * @throws Error if the payload length is not 32 bytes 22 | */ 23 | export function toPayload(msgHash: Hex | Uint8Array): number[] { 24 | const bytes = isBytes(msgHash) ? msgHash : toBytes(msgHash); 25 | if (bytes.length !== 32) { 26 | throw new Error(`Payload must have 32 bytes: ${msgHash}`); 27 | } 28 | return Array.from(bytes); 29 | } 30 | 31 | /** 32 | * Converts a payload array back to a hexadecimal string 33 | * 34 | * @param payload - The payload array to convert 35 | * @returns Hexadecimal string representation 36 | * @throws Error if the payload length is not 32 bytes 37 | */ 38 | export function fromPayload(payload: number[]): Hex { 39 | if (payload.length !== 32) { 40 | throw new Error(`Payload must have 32 bytes: ${payload}`); 41 | } 42 | // Convert number[] back to Uint8Array 43 | return toHex(new Uint8Array(payload)); 44 | } 45 | 46 | /** 47 | * Builds a transaction payload from a serialized transaction 48 | * 49 | * @param serializedTx - The serialized transaction 50 | * @returns Array of numbers representing the transaction payload 51 | */ 52 | export function buildTxPayload(serializedTx: `0x${string}`): number[] { 53 | return toPayload(keccak256(serializedTx)); 54 | } 55 | 56 | /** 57 | * Populates a transaction with necessary data 58 | * 59 | * @param tx - The base transaction data 60 | * @param from - The sender's address 61 | * @param client - Optional public client 62 | * @returns Complete transaction data 63 | * @throws Error if chain IDs don't match 64 | */ 65 | export async function populateTx( 66 | tx: BaseTx, 67 | from: Hex, 68 | client?: PublicClient 69 | ): Promise { 70 | const provider = client || Network.fromChainId(tx.chainId).client; 71 | const chainId = await provider.getChainId(); 72 | if (chainId !== tx.chainId) { 73 | // Can only happen when client is provided. 74 | throw new Error( 75 | `client chainId=${chainId} mismatch with tx.chainId=${tx.chainId}` 76 | ); 77 | } 78 | const transactionData = { 79 | nonce: tx.nonce ?? (await provider.getTransactionCount({ address: from })), 80 | account: from, 81 | to: tx.to, 82 | value: tx.value ?? 0n, 83 | data: tx.data ?? "0x", 84 | }; 85 | const [estimatedGas, { maxFeePerGas, maxPriorityFeePerGas }] = 86 | await Promise.all([ 87 | // Only estimate gas if not provided. 88 | tx.gas || provider.estimateGas(transactionData), 89 | provider.estimateFeesPerGas(), 90 | ]); 91 | return { 92 | ...transactionData, 93 | gas: estimatedGas, 94 | maxFeePerGas, 95 | maxPriorityFeePerGas, 96 | chainId, 97 | }; 98 | } 99 | 100 | /** 101 | * Adds a signature to a transaction 102 | * 103 | * @param params - Object containing transaction and signature 104 | * @returns Serialized signed transaction 105 | */ 106 | export function addSignature({ 107 | transaction, 108 | signature, 109 | }: TransactionWithSignature): Hex { 110 | const txData = parseTransaction(transaction); 111 | const signedTx = { 112 | ...signature, 113 | ...txData, 114 | }; 115 | return serializeTransaction(signedTx); 116 | } 117 | 118 | /** 119 | * Relays a signed transaction to the Ethereum mempool 120 | * 121 | * @param serializedTransaction - The signed transaction to relay 122 | * @param wait - Whether to wait for confirmation 123 | * @returns Transaction hash 124 | */ 125 | export async function relaySignedTransaction( 126 | serializedTransaction: Hex, 127 | wait: boolean = true 128 | ): Promise { 129 | const tx = parseTransaction(serializedTransaction); 130 | const network = Network.fromChainId(tx.chainId!); 131 | if (wait) { 132 | return network.client.sendRawTransaction({ serializedTransaction }); 133 | } else { 134 | network.client.sendRawTransaction({ serializedTransaction }); 135 | return keccak256(serializedTransaction); 136 | } 137 | } 138 | 139 | /** 140 | * Broadcasts a signed transaction to the Ethereum mempool 141 | * 142 | * @param tx - The signed transaction to broadcast 143 | * @returns Transaction hash 144 | */ 145 | export async function broadcastSignedTransaction( 146 | tx: TransactionWithSignature 147 | ): Promise { 148 | const signedTx = addSignature(tx); 149 | return relaySignedTransaction(signedTx); 150 | } 151 | -------------------------------------------------------------------------------- /tests/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../examples/setup"; 2 | import { mockAdapter, NearEthAdapter, Network } from "../src"; 3 | import { getBalance } from "viem/actions"; 4 | import dotenv from "dotenv"; 5 | import { 6 | recoverTypedDataAddress, 7 | recoverMessageAddress, 8 | zeroAddress, 9 | } from "viem"; 10 | dotenv.config({ 11 | override: true, 12 | quiet: true, 13 | }); 14 | 15 | describe("End To End", () => { 16 | let mockedAdapter: NearEthAdapter; 17 | let realAdapter: NearEthAdapter; 18 | const to = "0xdeADBeeF0000000000000000000000000b00B1e5"; 19 | const ONE_WEI = 1n; 20 | const chainId = SEPOLIA_CHAIN_ID; 21 | 22 | beforeAll(async () => { 23 | realAdapter = await setupNearEthAdapter(); 24 | mockedAdapter = await mockAdapter(process.env.ETH_PK! as `0x${string}`); 25 | }); 26 | 27 | afterAll(async () => { 28 | clearTimeout(undefined); 29 | }); 30 | 31 | it("Adapter.getBalance", async () => { 32 | await expect(realAdapter.getBalance(chainId)).resolves.toBeDefined(); 33 | }); 34 | 35 | it.skip("signAndSendTransaction", async () => { 36 | await expect( 37 | realAdapter.signAndSendTransaction({ 38 | to: realAdapter.address, 39 | value: ONE_WEI, 40 | chainId, 41 | }) 42 | ).resolves.toMatch(/^0x[a-fA-F0-9]{64}$/); // crude match for tx hash 43 | }, 20000); 44 | 45 | it.skip("signAndSendTransaction - Gnosis Chain", async () => { 46 | await expect( 47 | realAdapter.signAndSendTransaction({ 48 | // Sending 1 WEI to self (so we ~never run out of funds) 49 | to: realAdapter.address, 50 | value: ONE_WEI, 51 | // Gnosis Chain! 52 | chainId: 100, 53 | }) 54 | ).resolves.not.toThrow(); 55 | }); 56 | 57 | it("Fails to sign and send", async () => { 58 | const network = Network.fromChainId(chainId); 59 | const senderBalance = await getBalance(network.client, { 60 | address: mockedAdapter.address, 61 | }); 62 | await expect( 63 | mockedAdapter.signAndSendTransaction({ 64 | to, 65 | value: senderBalance + ONE_WEI, 66 | chainId, 67 | }) 68 | ).rejects.toThrow(); 69 | }, 15000); 70 | 71 | it("signMessage", async () => { 72 | const message = "NearEth"; 73 | const signature = await mockedAdapter.signMessage(message); 74 | const recoveredAddress = await recoverMessageAddress({ 75 | message, 76 | signature, 77 | }); 78 | expect(recoveredAddress).toBe(mockedAdapter.address); 79 | }); 80 | 81 | it("signTypedData", async () => { 82 | const message = { 83 | from: { 84 | name: "Cow", 85 | wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", 86 | }, 87 | to: { 88 | name: "Bob", 89 | wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", 90 | }, 91 | contents: "Hello, Bob!", 92 | } as const; 93 | 94 | const domain = { 95 | name: "Ether Mail", 96 | version: "1", 97 | chainId: 1, 98 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 99 | } as const; 100 | 101 | const types = { 102 | Person: [ 103 | { name: "name", type: "string" }, 104 | { name: "wallet", type: "address" }, 105 | ], 106 | Mail: [ 107 | { name: "from", type: "Person" }, 108 | { name: "to", type: "Person" }, 109 | { name: "contents", type: "string" }, 110 | ], 111 | } as const; 112 | 113 | const typedData = { 114 | types, 115 | primaryType: "Mail", 116 | message, 117 | domain, 118 | } as const; 119 | const signature = await mockedAdapter.signTypedData(typedData); 120 | 121 | const recoveredAddress = await recoverTypedDataAddress({ 122 | ...typedData, 123 | signature, 124 | }); 125 | expect(recoveredAddress).toBe(mockedAdapter.address); 126 | }); 127 | 128 | it("MPC: signAndSendSignRequest", async () => { 129 | const { nearPayload } = await realAdapter.encodeSignRequest({ 130 | method: "eth_sendTransaction", 131 | chainId: 11_155_111, // Sepolia 132 | params: [ 133 | { 134 | from: realAdapter.address, 135 | to: "0xdeADBeeF0000000000000000000000000b00B1e5", 136 | value: "0x01", // 1 WEI 137 | // data: "0x", // Optional 138 | }, 139 | ], 140 | }); 141 | const outcome = 142 | // @ts-expect-error: Property does not exist on IMpcContract 143 | await realAdapter.mpcContract.signAndSendSignRequest(nearPayload); 144 | expect(outcome.final_execution_status).toBe("EXECUTED"); 145 | }, 15000); 146 | 147 | it("Adapter: getSignatureRequestPayload", async () => { 148 | const { requestPayload } = await realAdapter.getSignatureRequestPayload({ 149 | chainId: 11_155_111, 150 | to: zeroAddress, 151 | }); 152 | expect(requestPayload.receiverId).toBe(realAdapter.mpcContract.accountId()); 153 | expect(requestPayload.signerId).toBe(realAdapter.nearAccountId()); 154 | expect(requestPayload.actions[0]!.params.methodName).toBe("sign"); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/network/index.ts: -------------------------------------------------------------------------------- 1 | import { Chain, createPublicClient, http, PublicClient } from "viem"; 2 | import * as chains from "viem/chains"; 3 | import { CHAIN_INFO } from "./constants"; 4 | 5 | /** Custom RPC endpoint overrides for specific chain IDs */ 6 | const rpcOverrides: { [key: number]: string } = { 7 | 43114: "https://avalanche.drpc.org", 8 | 11155111: "https://ethereum-sepolia-rpc.publicnode.com", 9 | }; 10 | 11 | /** Map of all supported networks exported by viem */ 12 | const SUPPORTED_NETWORKS = createNetworkMap(Object.values(chains)); 13 | 14 | /** Interface defining the required fields for a network configuration */ 15 | export interface NetworkFields { 16 | /** Display name of the network */ 17 | name: string; 18 | /** RPC endpoint URL */ 19 | rpcUrl: string; 20 | /** Unique chain identifier */ 21 | chainId: number; 22 | /** Block explorer URL */ 23 | scanUrl: string; 24 | /** Network logo URL */ 25 | icon: string | undefined; 26 | /** Whether this is a test network */ 27 | testnet: boolean; 28 | /** Native currency information */ 29 | nativeCurrency: { 30 | /** Number of decimal places */ 31 | decimals: number; 32 | /** Full name of the currency */ 33 | name: string; 34 | /** Currency symbol */ 35 | symbol: string; 36 | /** Address of wrapped token contract */ 37 | wrappedAddress: string | undefined; 38 | /** Currency logo URL (may differ from network icon) */ 39 | icon: string | undefined; 40 | }; 41 | } 42 | 43 | /** Interface defining optional configuration overrides for a Network instance */ 44 | interface NetworkOptions { 45 | /** Override the default RPC URL */ 46 | rpcUrl?: string; 47 | /** Override the default block explorer URL */ 48 | scanUrl?: string; 49 | } 50 | 51 | /** 52 | * Network class that provides access to network-specific data and functionality 53 | * Leverages network data provided through viem to make all relevant network fields 54 | * accessible dynamically by chain ID. 55 | */ 56 | export class Network implements NetworkFields { 57 | name: string; 58 | rpcUrl: string; 59 | chainId: number; 60 | scanUrl: string; 61 | client: PublicClient; 62 | icon: string | undefined; 63 | testnet: boolean; 64 | nativeCurrency: { 65 | decimals: number; 66 | name: string; 67 | symbol: string; 68 | wrappedAddress: string | undefined; 69 | icon: string | undefined; 70 | }; 71 | 72 | /** 73 | * Creates a new Network instance 74 | * 75 | * @param fields - Network configuration fields 76 | */ 77 | constructor({ 78 | name, 79 | rpcUrl, 80 | chainId, 81 | scanUrl, 82 | nativeCurrency, 83 | icon, 84 | }: NetworkFields) { 85 | const network = SUPPORTED_NETWORKS[chainId]!; 86 | 87 | this.name = name; 88 | this.rpcUrl = rpcUrl; 89 | this.chainId = chainId; 90 | this.scanUrl = scanUrl; 91 | this.client = createPublicClient({ 92 | transport: http(network.rpcUrl), 93 | }); 94 | this.testnet = network.testnet; 95 | this.nativeCurrency = nativeCurrency; 96 | this.icon = icon; 97 | } 98 | 99 | /** 100 | * Creates a Network instance from a chain ID 101 | * 102 | * @param chainId - The chain ID to create the network for 103 | * @param options - Optional configuration overrides 104 | * @returns A new Network instance 105 | * @throws Error if the chain ID is not supported 106 | */ 107 | static fromChainId(chainId: number, options: NetworkOptions = {}): Network { 108 | const networkFields = SUPPORTED_NETWORKS[chainId]; 109 | if (!networkFields) { 110 | throw new Error( 111 | `Network with chainId ${chainId} is not supported. 112 | Please reach out to the developers of https://github.com/bitteprotocol/near-ca` 113 | ); 114 | } 115 | const mergedFields = { 116 | ...networkFields, 117 | // Manual Settings. 118 | rpcUrl: options.rpcUrl || networkFields.rpcUrl, 119 | scanUrl: options.scanUrl || networkFields.scanUrl, 120 | }; 121 | 122 | return new Network(mergedFields); 123 | } 124 | } 125 | 126 | /** Mapping of chain IDs to their network configurations */ 127 | type NetworkMap = { [key: number]: NetworkFields }; 128 | 129 | /** 130 | * Creates a map of network configurations indexed by chain ID 131 | * 132 | * @param supportedNetworks - Array of Chain objects from viem 133 | * @returns A map of network configurations 134 | */ 135 | function createNetworkMap(supportedNetworks: Chain[]): NetworkMap { 136 | const networkMap: NetworkMap = {}; 137 | supportedNetworks.forEach((network) => { 138 | const chainInfo = CHAIN_INFO[network.id]; 139 | const icon = chainInfo?.icon || `/${network.nativeCurrency.symbol}.svg`; 140 | networkMap[network.id] = { 141 | name: network.name, 142 | rpcUrl: rpcOverrides[network.id] || network.rpcUrls.default.http[0]!, 143 | chainId: network.id, 144 | scanUrl: network.blockExplorers?.default.url || "", 145 | icon, 146 | testnet: network.testnet || false, 147 | nativeCurrency: { 148 | ...network.nativeCurrency, 149 | wrappedAddress: chainInfo?.wrappedToken, 150 | icon: chainInfo?.currencyIcon || icon, 151 | }, 152 | }; 153 | }); 154 | 155 | return networkMap; 156 | } 157 | 158 | /** 159 | * Checks if a given chain ID corresponds to a test network 160 | * 161 | * @param chainId - The chain ID to check 162 | * @returns True if the network is a testnet, false otherwise 163 | */ 164 | export function isTestnet(chainId: number): boolean { 165 | return Network.fromChainId(chainId).testnet; 166 | } 167 | -------------------------------------------------------------------------------- /src/mpcContract.ts: -------------------------------------------------------------------------------- 1 | import { Account, transactions } from "near-api-js"; 2 | import { Address, Signature } from "viem"; 3 | import { 4 | deriveChildPublicKey, 5 | najPublicKeyStrToUncompressedHexPoint, 6 | signatureFromOutcome, 7 | uncompressedHexPointToEvmAddress, 8 | } from "./utils"; 9 | import { TGAS } from "./chains"; 10 | import { FunctionCallTransaction, SignArgs } from "./types"; 11 | import { FinalExecutionOutcome } from "near-api-js/lib/providers"; 12 | 13 | /** 14 | * Near Contract Type for change methods. 15 | * 16 | * @typeParam T - The type of the arguments for the change method 17 | */ 18 | export interface ChangeMethodArgs { 19 | /** Change method function arguments */ 20 | args: T; 21 | /** Gas limit on transaction execution */ 22 | gas: string; 23 | /** Account signing the call */ 24 | signerAccount: Account; 25 | /** Attached deposit (i.e., payable amount) to attach to the transaction */ 26 | amount: string; 27 | } 28 | 29 | /** 30 | * High-level interface for the Near MPC-Recovery Contract 31 | * located in: https://github.com/near/mpc-recovery 32 | */ 33 | export class MpcContract implements IMpcContract { 34 | rootPublicKey: string | undefined; 35 | contractId: string; 36 | // contract: MpcContractInterface; 37 | connectedAccount: Account; 38 | 39 | /** 40 | * Creates a new MPC Contract instance 41 | * 42 | * @param account - The NEAR account to use 43 | * @param contractId - The contract ID 44 | * @param rootPublicKey - Optional root public key 45 | */ 46 | constructor(account: Account, contractId: string, rootPublicKey?: string) { 47 | this.connectedAccount = account; 48 | this.rootPublicKey = rootPublicKey; 49 | this.contractId = contractId; 50 | } 51 | 52 | /** 53 | * Gets the contract ID 54 | * 55 | * @returns The contract ID 56 | */ 57 | accountId(): string { 58 | return this.contractId; 59 | } 60 | 61 | /** 62 | * Derives an Ethereum address from a derivation path 63 | * 64 | * @param derivationPath - The path to derive the address from 65 | * @returns The derived Ethereum address 66 | */ 67 | deriveEthAddress = async (derivationPath: string): Promise
=> { 68 | if (!this.rootPublicKey) { 69 | this.rootPublicKey = await this.connectedAccount.provider.callFunction( 70 | this.contractId, 71 | "public_key", 72 | {} 73 | ); 74 | } 75 | 76 | const publicKey = deriveChildPublicKey( 77 | najPublicKeyStrToUncompressedHexPoint(this.rootPublicKey!), 78 | this.connectedAccount.accountId, 79 | derivationPath 80 | ); 81 | 82 | return uncompressedHexPointToEvmAddress(publicKey); 83 | }; 84 | 85 | /** 86 | * Requests a signature from the MPC contract 87 | * 88 | * @param signArgs - The arguments for the signature request 89 | * @param gas - Optional gas limit 90 | * @returns The signature 91 | */ 92 | requestSignature = async ( 93 | signArgs: SignArgs, 94 | gas?: bigint 95 | ): Promise => { 96 | const transaction = await this.encodeSignatureRequestTx(signArgs, gas); 97 | const outcome = await this.signAndSendSignRequest(transaction); 98 | return signatureFromOutcome(outcome); 99 | }; 100 | 101 | /** 102 | * Encodes a signature request into a transaction 103 | * 104 | * @param signArgs - The arguments for the signature request 105 | * @param gas - Optional gas limit 106 | * @returns The encoded transaction 107 | */ 108 | async encodeSignatureRequestTx( 109 | signArgs: SignArgs, 110 | gas?: bigint 111 | ): Promise> { 112 | return { 113 | signerId: this.connectedAccount.accountId, 114 | receiverId: this.contractId, 115 | actions: [ 116 | { 117 | type: "FunctionCall", 118 | params: { 119 | methodName: "sign", 120 | args: { request: signArgs }, 121 | gas: gasOrDefault(gas), 122 | deposit: "1", 123 | }, 124 | }, 125 | ], 126 | }; 127 | } 128 | 129 | /** 130 | * Signs and sends a signature request 131 | * 132 | * @param transaction - The transaction to sign and send 133 | * @param blockTimeout - Optional timeout in blocks 134 | * @returns The execution outcome 135 | */ 136 | async signAndSendSignRequest( 137 | transaction: FunctionCallTransaction<{ request: SignArgs }> 138 | ): Promise { 139 | const account = this.connectedAccount; 140 | const signedTx = await account.createSignedTransaction( 141 | this.contractId, 142 | transaction.actions.map(({ params: { args, gas, deposit } }) => 143 | transactions.functionCall("sign", args, BigInt(gas), BigInt(deposit)) 144 | ) 145 | ); 146 | return account.provider.sendTransactionUntil(signedTx, "EXECUTED"); 147 | } 148 | } 149 | 150 | /** 151 | * Returns the gas value or a default if not provided 152 | * 153 | * @param gas - Optional gas value 154 | * @returns The gas value as a string 155 | */ 156 | function gasOrDefault(gas?: bigint): string { 157 | if (gas !== undefined) { 158 | return gas.toString(); 159 | } 160 | return (TGAS * 250n).toString(); 161 | } 162 | 163 | /** Interface for MPC Contract implementation */ 164 | export interface IMpcContract { 165 | connectedAccount: Account; 166 | accountId(): string; 167 | deriveEthAddress(derivationPath: string): Promise
; 168 | requestSignature(signArgs: SignArgs, gas?: bigint): Promise; 169 | encodeSignatureRequestTx( 170 | signArgs: SignArgs, 171 | gas?: bigint 172 | ): Promise>; 173 | } 174 | -------------------------------------------------------------------------------- /src/types/guards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hex, 3 | isAddress, 4 | isHex, 5 | parseTransaction, 6 | serializeTransaction, 7 | TransactionSerializable, 8 | TypedDataDomain, 9 | } from "viem"; 10 | import { 11 | EIP712TypedData, 12 | KeyPairString, 13 | SignMethod, 14 | TypedMessageTypes, 15 | } from "."; 16 | 17 | /** 18 | * Type guard to check if a value is a valid SignMethod 19 | * 20 | * @param method - The value to check 21 | * @returns True if the value is a valid SignMethod 22 | */ 23 | export function isSignMethod(method: unknown): method is SignMethod { 24 | return ( 25 | typeof method === "string" && 26 | [ 27 | "eth_sign", 28 | "personal_sign", 29 | "eth_sendTransaction", 30 | "eth_signTypedData", 31 | "eth_signTypedData_v4", 32 | ].includes(method) 33 | ); 34 | } 35 | 36 | /** 37 | * Type guard to check if a value is a valid TypedDataDomain 38 | * Validates all optional properties according to EIP-712 specification 39 | * 40 | * @param domain - The value to check 41 | * @returns True if the value matches TypedDataDomain structure 42 | */ 43 | export const isTypedDataDomain = ( 44 | domain: unknown 45 | ): domain is TypedDataDomain => { 46 | if (typeof domain !== "object" || domain === null) return false; 47 | 48 | const candidate = domain as Record; 49 | 50 | // Check that all properties, if present, are of the correct type 51 | return Object.entries(candidate).every(([key, value]) => { 52 | switch (key) { 53 | case "chainId": 54 | return ( 55 | typeof value === "undefined" || 56 | typeof value === "number" || 57 | isHex(value) || 58 | (typeof value === "string" && typeof parseInt(value) === "number") 59 | ); 60 | case "name": 61 | case "version": 62 | return typeof value === "undefined" || typeof value === "string"; 63 | case "verifyingContract": 64 | return ( 65 | typeof value === "undefined" || 66 | (typeof value === "string" && isAddress(value)) 67 | ); 68 | case "salt": 69 | return typeof value === "undefined" || typeof value === "string"; 70 | default: 71 | return false; // Reject unknown properties 72 | } 73 | }); 74 | }; 75 | 76 | /** 77 | * Type guard to check if a value matches the TypedMessageTypes structure 78 | * 79 | * @param types - The value to check 80 | * @returns True if the value matches TypedMessageTypes structure 81 | */ 82 | const isTypedMessageTypes = (types: unknown): types is TypedMessageTypes => { 83 | if (typeof types !== "object" || types === null) return false; 84 | 85 | return Object.entries(types).every(([_, value]) => { 86 | return ( 87 | Array.isArray(value) && 88 | value.every( 89 | (item) => 90 | typeof item === "object" && 91 | item !== null && 92 | "name" in item && 93 | "type" in item && 94 | typeof item.name === "string" && 95 | typeof item.type === "string" 96 | ) 97 | ); 98 | }); 99 | }; 100 | 101 | /** 102 | * Type guard to check if a value is a valid EIP712TypedData 103 | * Validates the structure according to EIP-712 specification 104 | * 105 | * @param obj - The value to check 106 | * @returns True if the value matches EIP712TypedData structure 107 | */ 108 | export const isEIP712TypedData = (obj: unknown): obj is EIP712TypedData => { 109 | if (typeof obj !== "object" || obj === null) return false; 110 | 111 | const candidate = obj as Record; 112 | 113 | return ( 114 | "domain" in candidate && 115 | "types" in candidate && 116 | "message" in candidate && 117 | "primaryType" in candidate && 118 | isTypedDataDomain(candidate.domain) && 119 | isTypedMessageTypes(candidate.types) && 120 | typeof candidate.message === "object" && 121 | candidate.message !== null && 122 | typeof candidate.primaryType === "string" 123 | ); 124 | }; 125 | 126 | /** 127 | * Type guard to check if a value can be serialized as an Ethereum transaction 128 | * Attempts to serialize the input and returns true if successful 129 | * 130 | * @param data - The value to check 131 | * @returns True if the value can be serialized as a transaction 132 | */ 133 | export function isTransactionSerializable( 134 | data: unknown 135 | ): data is TransactionSerializable { 136 | try { 137 | serializeTransaction(data as TransactionSerializable); 138 | return true; 139 | } catch (error) { 140 | return false; 141 | } 142 | } 143 | 144 | /** 145 | * Type guard to check if a value is a valid RLP-encoded transaction hex string 146 | * Attempts to parse the input as a transaction and returns true if successful 147 | * 148 | * @param data - The value to check 149 | * @returns True if the value is a valid RLP-encoded transaction hex 150 | */ 151 | export function isRlpHex(data: unknown): data is Hex { 152 | try { 153 | parseTransaction(data as Hex); 154 | return true; 155 | } catch (error) { 156 | return false; 157 | } 158 | } 159 | 160 | /** 161 | * Type guard to check if a value is a valid NEAR key pair string 162 | * 163 | * @param value - The value to check 164 | * @returns True if the value is a valid KeyPairString format 165 | * @example 166 | * ```ts 167 | * isKeyPairString("ed25519:ABC123") // true 168 | * isKeyPairString("secp256k1:DEF456") // true 169 | * isKeyPairString("invalid:GHI789") // false 170 | * isKeyPairString("ed25519") // false 171 | * ``` 172 | */ 173 | export function isKeyPairString(value: unknown): value is KeyPairString { 174 | if (typeof value !== "string") return false; 175 | 176 | const [prefix, key] = value.split(":"); 177 | 178 | // Check if we have both parts and the prefix is valid 179 | if (!prefix || !key || !["ed25519", "secp256k1"].includes(prefix)) { 180 | return false; 181 | } 182 | 183 | // Check if the key part exists and is non-empty 184 | return key.length > 0; 185 | } 186 | -------------------------------------------------------------------------------- /examples/abis/WETH.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "guy", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "wad", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "src", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "dst", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "wad", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": false, 82 | "inputs": [ 83 | { 84 | "name": "wad", 85 | "type": "uint256" 86 | } 87 | ], 88 | "name": "withdraw", 89 | "outputs": [], 90 | "payable": false, 91 | "stateMutability": "nonpayable", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [], 97 | "name": "decimals", 98 | "outputs": [ 99 | { 100 | "name": "", 101 | "type": "uint8" 102 | } 103 | ], 104 | "payable": false, 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "constant": true, 110 | "inputs": [ 111 | { 112 | "name": "", 113 | "type": "address" 114 | } 115 | ], 116 | "name": "balanceOf", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "uint256" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": true, 129 | "inputs": [], 130 | "name": "symbol", 131 | "outputs": [ 132 | { 133 | "name": "", 134 | "type": "string" 135 | } 136 | ], 137 | "payable": false, 138 | "stateMutability": "view", 139 | "type": "function" 140 | }, 141 | { 142 | "constant": false, 143 | "inputs": [ 144 | { 145 | "name": "dst", 146 | "type": "address" 147 | }, 148 | { 149 | "name": "wad", 150 | "type": "uint256" 151 | } 152 | ], 153 | "name": "transfer", 154 | "outputs": [ 155 | { 156 | "name": "", 157 | "type": "bool" 158 | } 159 | ], 160 | "payable": false, 161 | "stateMutability": "nonpayable", 162 | "type": "function" 163 | }, 164 | { 165 | "constant": false, 166 | "inputs": [], 167 | "name": "deposit", 168 | "outputs": [], 169 | "payable": true, 170 | "stateMutability": "payable", 171 | "type": "function" 172 | }, 173 | { 174 | "constant": true, 175 | "inputs": [ 176 | { 177 | "name": "", 178 | "type": "address" 179 | }, 180 | { 181 | "name": "", 182 | "type": "address" 183 | } 184 | ], 185 | "name": "allowance", 186 | "outputs": [ 187 | { 188 | "name": "", 189 | "type": "uint256" 190 | } 191 | ], 192 | "payable": false, 193 | "stateMutability": "view", 194 | "type": "function" 195 | }, 196 | { 197 | "payable": true, 198 | "stateMutability": "payable", 199 | "type": "fallback" 200 | }, 201 | { 202 | "anonymous": false, 203 | "inputs": [ 204 | { 205 | "indexed": true, 206 | "name": "src", 207 | "type": "address" 208 | }, 209 | { 210 | "indexed": true, 211 | "name": "guy", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": false, 216 | "name": "wad", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "Approval", 221 | "type": "event" 222 | }, 223 | { 224 | "anonymous": false, 225 | "inputs": [ 226 | { 227 | "indexed": true, 228 | "name": "src", 229 | "type": "address" 230 | }, 231 | { 232 | "indexed": true, 233 | "name": "dst", 234 | "type": "address" 235 | }, 236 | { 237 | "indexed": false, 238 | "name": "wad", 239 | "type": "uint256" 240 | } 241 | ], 242 | "name": "Transfer", 243 | "type": "event" 244 | }, 245 | { 246 | "anonymous": false, 247 | "inputs": [ 248 | { 249 | "indexed": true, 250 | "name": "dst", 251 | "type": "address" 252 | }, 253 | { 254 | "indexed": false, 255 | "name": "wad", 256 | "type": "uint256" 257 | } 258 | ], 259 | "name": "Deposit", 260 | "type": "event" 261 | }, 262 | { 263 | "anonymous": false, 264 | "inputs": [ 265 | { 266 | "indexed": true, 267 | "name": "src", 268 | "type": "address" 269 | }, 270 | { 271 | "indexed": false, 272 | "name": "wad", 273 | "type": "uint256" 274 | } 275 | ], 276 | "name": "Withdrawal", 277 | "type": "event" 278 | } 279 | ] -------------------------------------------------------------------------------- /tests/unit/types.guards.test.ts: -------------------------------------------------------------------------------- 1 | import { TransactionSerializable } from "viem"; 2 | import { 3 | isEIP712TypedData, 4 | isRlpHex, 5 | isSignMethod, 6 | isTransactionSerializable, 7 | isTypedDataDomain, 8 | } from "../../src/"; 9 | 10 | const validEIP1559Transaction: TransactionSerializable = { 11 | to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 12 | value: BigInt(1000000000000000000), // 1 ETH 13 | chainId: 1, 14 | maxFeePerGas: 1n, 15 | }; 16 | 17 | const commonInvalidCases = [ 18 | null, 19 | undefined, 20 | {}, 21 | { to: "invalid-address" }, 22 | { value: "not-a-bigint" }, 23 | { chainId: "not-a-number" }, 24 | "random string", 25 | 123, 26 | [], 27 | ]; 28 | 29 | describe("SignMethod", () => { 30 | it("returns true for all valid SignMethods", async () => { 31 | [ 32 | "eth_sign", 33 | "personal_sign", 34 | "eth_sendTransaction", 35 | "eth_signTypedData", 36 | "eth_signTypedData_v4", 37 | ].map((item) => expect(isSignMethod(item)).toBe(true)); 38 | }); 39 | 40 | it("returns false for invalid data inputs", async () => { 41 | ["poop", undefined, false, 1, {}].map((item) => 42 | expect(isSignMethod(item)).toBe(false) 43 | ); 44 | }); 45 | }); 46 | describe("isEIP712TypedData", () => { 47 | it("returns true for valid EIP712TypedData", async () => { 48 | const message = { 49 | from: { 50 | name: "Cow", 51 | wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", 52 | }, 53 | to: { 54 | name: "Bob", 55 | wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", 56 | }, 57 | contents: "Hello, Bob!", 58 | } as const; 59 | 60 | const domain = { 61 | name: "Ether Mail", 62 | version: "1", 63 | chainId: 1, 64 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 65 | } as const; 66 | 67 | const types = { 68 | Person: [ 69 | { name: "name", type: "string" }, 70 | { name: "wallet", type: "address" }, 71 | ], 72 | Mail: [ 73 | { name: "from", type: "Person" }, 74 | { name: "to", type: "Person" }, 75 | { name: "contents", type: "string" }, 76 | ], 77 | } as const; 78 | 79 | const typedData = { 80 | types, 81 | primaryType: "Mail", 82 | message, 83 | domain, 84 | } as const; 85 | expect(isEIP712TypedData(typedData)).toBe(true); 86 | }); 87 | 88 | it("returns false for invalid data inputs", async () => { 89 | commonInvalidCases.map((item) => 90 | expect(isEIP712TypedData(item)).toBe(false) 91 | ); 92 | }); 93 | 94 | it("recognizes safe EIP712 Typed data (with hex chainId)", async () => { 95 | const safeTypedData = { 96 | types: { 97 | SafeTx: [ 98 | { name: "to", type: "address" }, 99 | { name: "value", type: "uint256" }, 100 | { name: "data", type: "bytes" }, 101 | { name: "operation", type: "uint8" }, 102 | { name: "safeTxGas", type: "uint256" }, 103 | { name: "baseGas", type: "uint256" }, 104 | { name: "gasPrice", type: "uint256" }, 105 | { name: "gasToken", type: "address" }, 106 | { name: "refundReceiver", type: "address" }, 107 | { name: "nonce", type: "uint256" }, 108 | ], 109 | EIP712Domain: [ 110 | { name: "chainId", type: "uint256" }, 111 | { name: "verifyingContract", type: "address" }, 112 | ], 113 | }, 114 | domain: { 115 | chainId: "0xaa36a7", 116 | verifyingContract: "0x7fa8e8264985c7525fc50f98ac1a9b3765405489", 117 | }, 118 | primaryType: "SafeTx", 119 | message: { 120 | to: "0x102543f7e6b5786a444cc89ff73012825d13000d", 121 | value: "100000000000000000", 122 | data: "0x", 123 | operation: "0", 124 | safeTxGas: "0", 125 | baseGas: "0", 126 | gasPrice: "0", 127 | gasToken: "0x0000000000000000000000000000000000000000", 128 | refundReceiver: "0x0000000000000000000000000000000000000000", 129 | nonce: "0", 130 | }, 131 | }; 132 | 133 | expect(isEIP712TypedData(safeTypedData)).toBe(true); 134 | }); 135 | }); 136 | 137 | describe("isTransactionSerializable", () => { 138 | it("should return true for valid transaction data", () => { 139 | expect(isTransactionSerializable(validEIP1559Transaction)).toBe(true); 140 | }); 141 | 142 | it("should return false for invalid transaction data", () => { 143 | commonInvalidCases.forEach((testCase) => { 144 | expect(isTransactionSerializable(testCase)).toBe(false); 145 | }); 146 | }); 147 | }); 148 | 149 | describe("isTypedDataDomain", () => { 150 | it("returns true for various valid domains", async () => { 151 | const permit2Domain = { 152 | name: "Permit2", 153 | chainId: "43114", 154 | verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3", 155 | }; 156 | expect(isTypedDataDomain(permit2Domain)).toBe(true); 157 | expect( 158 | isTypedDataDomain({ 159 | name: "Ether Mail", 160 | version: "1", 161 | chainId: 1, 162 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 163 | }) 164 | ).toBe(true); 165 | expect( 166 | isTypedDataDomain({ 167 | chainId: "0xaa36a7", 168 | verifyingContract: "0x7fa8e8264985c7525fc50f98ac1a9b3765405489", 169 | }) 170 | ).toBe(true); 171 | }); 172 | }); 173 | 174 | describe("isRlpHex", () => { 175 | it("should return true for valid RLP-encoded transaction hex", () => { 176 | // This is an example of a valid RLP-encoded transaction hex: 177 | 178 | // serializeTransaction(validEIP1559Transaction) 179 | const validRlpHex = 180 | "0x02e501808001809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0"; 181 | expect(isRlpHex(validRlpHex)).toBe(true); 182 | }); 183 | 184 | it("should return false for invalid RLP hex data", () => { 185 | const invalidCases = [ 186 | null, 187 | undefined, 188 | {}, 189 | "not-a-hex", 190 | "0x", // empty hex 191 | "0x1234", // too short 192 | "0xinvalid", 193 | 123, 194 | [], 195 | // Invalid RLP structure but valid hex 196 | "0x1234567890abcdef", 197 | ]; 198 | 199 | invalidCases.forEach((testCase) => { 200 | expect(isRlpHex(testCase)).toBe(false); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /src/utils/signature.ts: -------------------------------------------------------------------------------- 1 | import { Signature } from "viem"; 2 | import { MPCSignature } from "../types"; 3 | import { 4 | FinalExecutionOutcome, 5 | FinalExecutionStatus, 6 | } from "near-api-js/lib/providers"; 7 | 8 | /** Basic structure of the JSON-RPC response */ 9 | export interface JSONRPCResponse { 10 | /** JSON-RPC version */ 11 | jsonrpc: string; 12 | /** Request identifier */ 13 | id: number | string | null; 14 | /** Response result */ 15 | result?: T; 16 | /** Error information if request failed */ 17 | error?: { 18 | /** Error code */ 19 | code: number; 20 | /** Error message */ 21 | message: string; 22 | /** Additional error data */ 23 | data?: unknown; 24 | }; 25 | } 26 | 27 | /** 28 | * Retrieves a signature from a transaction hash 29 | * 30 | * @param nodeUrl - URL of the NEAR node 31 | * @param txHash - Transaction hash to query 32 | * @param accountId - Account ID used to determine shard for query (defaults to "non-empty") 33 | * @returns The signature from the transaction 34 | * @throws Error if HTTP request fails or response is invalid 35 | */ 36 | export async function signatureFromTxHash( 37 | nodeUrl: string, 38 | txHash: string, 39 | /// This field doesn't appear to be necessary although (possibly for efficiency), 40 | /// the docs mention that it is "used to determine which shard to query for transaction". 41 | accountId: string = "non-empty" 42 | ): Promise { 43 | const payload = { 44 | jsonrpc: "2.0", 45 | id: "dontcare", 46 | // This could be replaced with `tx`. 47 | // method: "tx", 48 | method: "EXPERIMENTAL_tx_status", 49 | params: [txHash, accountId], 50 | }; 51 | 52 | // Make the POST request with the fetch API 53 | const response = await fetch(nodeUrl, { 54 | method: "POST", 55 | headers: { 56 | "Content-Type": "application/json", 57 | }, 58 | body: JSON.stringify(payload), 59 | }); 60 | if (!response.ok) { 61 | throw new Error(`HTTP error! status: ${response.status}`); 62 | } 63 | 64 | const json: JSONRPCResponse = await response.json(); 65 | if (json.error) { 66 | throw new Error(`JSON-RPC error: ${json.error.message}`); 67 | } 68 | if ( 69 | typeof json.result?.status === "object" && 70 | "Failure" in json.result.status 71 | ) { 72 | const message = JSON.stringify(json.result.status.Failure); 73 | throw new Error( 74 | `Signature Request Failed in ${txHash} with message: ${message}` 75 | ); 76 | } 77 | 78 | if (json.result) { 79 | return signatureFromOutcome(json.result); 80 | } else { 81 | throw new Error(`No FinalExecutionOutcome in response: ${json}`); 82 | } 83 | } 84 | 85 | /** 86 | * Transforms an MPC signature into a standard Ethereum signature 87 | * 88 | * @param mpcSig - The MPC signature to transform 89 | * @returns Standard Ethereum signature 90 | */ 91 | export function transformSignature(mpcSig: MPCSignature): Signature { 92 | const { big_r, s, recovery_id } = mpcSig; 93 | return { 94 | r: `0x${big_r.affine_point.substring(2)}`, 95 | s: `0x${s.scalar}`, 96 | yParity: recovery_id, 97 | }; 98 | } 99 | 100 | /** 101 | * Extracts a signature from a transaction outcome 102 | * 103 | * @param outcome - Transaction outcome from NEAR API 104 | * @returns The extracted signature 105 | * @throws Error if signature is not found or is invalid 106 | * @remarks 107 | * Handles both standard and relayed signature requests. For relayed requests, 108 | * extracts signature from receipts_outcome, taking the second occurrence as 109 | * the first is nested inside `{ Ok: MPCSignature }`. 110 | */ 111 | export function signatureFromOutcome( 112 | // The Partial object is intended to make up for the 113 | // difference between all the different near-api versions and wallet-selector bullshit 114 | // the field `final_execution_status` is in one, but not the other, and we don't use it anyway. 115 | outcome: 116 | | FinalExecutionOutcome 117 | | Omit 118 | ): Signature { 119 | const txHash = outcome.transaction_outcome?.id; 120 | // TODO - find a scenario when outcome.status is `FinalExecutionStatusBasic`! 121 | let b64Sig = (outcome.status as FinalExecutionStatus).SuccessValue; 122 | if (!b64Sig) { 123 | // This scenario occurs when sign call is relayed (i.e. executed by someone else). 124 | // E.g. https://testnet.nearblocks.io/txns/G1f1HVUxDBWXAEimgNWobQ9yCx1EgA2tzYHJBFUfo3dj 125 | // We have to dig into `receipts_outcome` and extract the signature from within. 126 | // We want the second occurrence of the signature because 127 | // the first is nested inside `{ Ok: MPCSignature }`) 128 | b64Sig = outcome.receipts_outcome 129 | // Map to get SuccessValues: The Signature will appear twice. 130 | .map( 131 | (receipt) => 132 | (receipt.outcome.status as FinalExecutionStatus).SuccessValue 133 | ) 134 | // Reverse to "find" the last non-empty value! 135 | .reverse() 136 | .find((value) => value && value.trim().length > 0); 137 | } 138 | if (!b64Sig) { 139 | throw new Error(`No detectable signature found in transaction ${txHash}`); 140 | } 141 | if (b64Sig === "eyJFcnIiOiJGYWlsZWQifQ==") { 142 | // {"Err": "Failed"} 143 | throw new Error(`Signature Request Failed in ${txHash}`); 144 | } 145 | const decodedValue = Buffer.from(b64Sig, "base64").toString("utf-8"); 146 | const signature = JSON.parse(decodedValue); 147 | if (isMPCSignature(signature)) { 148 | return transformSignature(signature); 149 | } else { 150 | throw new Error(`No detectable signature found in transaction ${txHash}`); 151 | } 152 | } 153 | 154 | /** 155 | * Type guard to check if an object is a valid MPC signature 156 | * E.g. 157 | * { 158 | * big_r: { 159 | * affine_point: 160 | * "0337F110D095850FD1D6451B30AF40C15A82566C7FA28997D3EF83C5588FBAF99C", 161 | * }, 162 | * s: { 163 | * scalar: 164 | * "4C5D1C3A8CAFF5F0C13E34B4258D114BBEAB99D51AF31648482B7597F3AD5B72", 165 | * }, 166 | * recovery_id: 1, 167 | * } 168 | * @param obj - The object to check 169 | * @returns True if the object matches MPCSignature structure 170 | */ 171 | function isMPCSignature(obj: unknown): obj is MPCSignature { 172 | return ( 173 | typeof obj === "object" && 174 | obj !== null && 175 | typeof (obj as MPCSignature).big_r === "object" && 176 | typeof (obj as MPCSignature).big_r.affine_point === "string" && 177 | typeof (obj as MPCSignature).s === "object" && 178 | typeof (obj as MPCSignature).s.scalar === "string" && 179 | typeof (obj as MPCSignature).recovery_id === "number" 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /tests/unit/wc.handlers.test.ts: -------------------------------------------------------------------------------- 1 | import { Hex, isHex, toHex } from "viem"; 2 | import { requestRouter } from "../../src"; 3 | 4 | describe("Wallet Connect", () => { 5 | const chainId = 11155111; 6 | const from = "0xa61d98854f7ab25402e3d12548a2e93a080c1f97" as Hex; 7 | const to = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14" as Hex; 8 | 9 | describe("requestRouter: eth_sign & personal_sign", () => { 10 | it("hello message", async () => { 11 | const messageString = "Hello!"; 12 | const { evmMessage, hashToSign } = await requestRouter({ 13 | method: "eth_sign", 14 | chainId, 15 | params: [from, toHex(messageString)], 16 | }); 17 | expect(evmMessage).toEqual(messageString); 18 | expect(hashToSign).toEqual( 19 | "0x52b6437db56d87f5991d7c173cf11b9dd0f9fb083260bef1bf0c338042bc398c" 20 | ); 21 | }); 22 | 23 | it("opensea login", async () => { 24 | const { evmMessage, hashToSign } = await requestRouter({ 25 | method: "personal_sign", 26 | chainId, 27 | params: [ 28 | "0x57656c636f6d6520746f204f70656e536561210a0a436c69636b20746f207369676e20696e20616e642061636365707420746865204f70656e536561205465726d73206f662053657276696365202868747470733a2f2f6f70656e7365612e696f2f746f732920616e64205072697661637920506f6c696379202868747470733a2f2f6f70656e7365612e696f2f70726976616379292e0a0a5468697320726571756573742077696c6c206e6f742074726967676572206120626c6f636b636861696e207472616e73616374696f6e206f7220636f737420616e792067617320666565732e0a0a57616c6c657420616464726573733a0a3078663131633232643631656364376231616463623662343335343266653861393662393332386463370a0a4e6f6e63653a0a32393731633731312d623739382d343433342d613633312d316333663133656665353365", 29 | "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", 30 | ], 31 | }); 32 | expect(evmMessage).toEqual( 33 | `Welcome to OpenSea! 34 | 35 | Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy). 36 | 37 | This request will not trigger a blockchain transaction or cost any gas fees. 38 | 39 | Wallet address: 40 | 0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7 41 | 42 | Nonce: 43 | 2971c711-b798-4434-a631-1c3f13efe53e` 44 | ); 45 | expect(hashToSign).toEqual( 46 | "0x4701732655667bb0a8b48dd95c5ca6045bcb6fe9cba93f0dcbbaa102f9c3e7db" 47 | ); 48 | }); 49 | 50 | it("manifold login", async () => { 51 | const { evmMessage, hashToSign } = await requestRouter({ 52 | method: "personal_sign", 53 | chainId, 54 | params: [ 55 | "0x506c65617365207369676e2074686973206d65737361676520746f20616363657373204d616e69666f6c642053747564696f0a0a4368616c6c656e67653a2034313133666333616232636336306635643539356232653535333439663165656335366664306337306434323837303831666537313536383438323633363236", 56 | "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", 57 | ], 58 | }); 59 | expect(evmMessage).toEqual( 60 | `Please sign this message to access Manifold Studio 61 | 62 | Challenge: 4113fc3ab2cc60f5d595b2e55349f1eec56fd0c70d4287081fe7156848263626` 63 | ); 64 | expect(hashToSign).toEqual( 65 | "0x68011f51c8b1a56c29f1d130c481d7564966fee51b6b3b6ed621dd2d9cc5a4d3" 66 | ); 67 | }); 68 | }); 69 | describe("requestRouter: eth_sendTransaction", () => { 70 | /// can't test payload: its non-deterministic because of gas values! 71 | it("with value", async () => { 72 | const { evmMessage } = await requestRouter({ 73 | method: "eth_sendTransaction", 74 | chainId, 75 | params: [ 76 | { 77 | gas: "0xd31d", 78 | value: "0x16345785d8a0000", 79 | from, 80 | to, 81 | data: "0xd0e30db0", 82 | }, 83 | ], 84 | }); 85 | expect(isHex(evmMessage)).toBe(true); 86 | }); 87 | 88 | it("null value", async () => { 89 | const { evmMessage } = await requestRouter({ 90 | method: "eth_sendTransaction", 91 | chainId, 92 | params: [ 93 | { 94 | gas: "0xa8c3", 95 | from, 96 | to, 97 | data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000", 98 | }, 99 | ], 100 | }); 101 | 102 | expect(isHex(evmMessage)).toBe(true); 103 | }); 104 | 105 | it("null data", async () => { 106 | const { evmMessage } = await requestRouter({ 107 | method: "eth_sendTransaction", 108 | chainId, 109 | params: [ 110 | { 111 | gas: "0xa8c3", 112 | from, 113 | to, 114 | value: "0x01", 115 | }, 116 | ], 117 | }); 118 | 119 | expect(isHex(evmMessage)).toBe(true); 120 | }); 121 | }); 122 | describe("requestRouter: eth_signTypedData", () => { 123 | it("Cowswap Order", async () => { 124 | const jsonStr = `{ 125 | "types": { 126 | "Permit": [ 127 | {"name": "owner", "type": "address"}, 128 | {"name": "spender", "type": "address"}, 129 | {"name": "value", "type": "uint256"}, 130 | {"name": "nonce", "type": "uint256"}, 131 | {"name": "deadline", "type": "uint256"} 132 | ], 133 | "EIP712Domain": [ 134 | {"name": "name", "type": "string"}, 135 | {"name": "version", "type": "string"}, 136 | {"name": "chainId", "type": "uint256"}, 137 | {"name": "verifyingContract", "type": "address"} 138 | ] 139 | }, 140 | "domain": { 141 | "name": "CoW Protocol Token", 142 | "version": "1", 143 | "chainId": "11155111", 144 | "verifyingContract": "0x0625afb445c3b6b7b929342a04a22599fd5dbb59" 145 | }, 146 | "primaryType": "Permit", 147 | "message": { 148 | "owner": "0xa61d98854f7ab25402e3d12548a2e93a080c1f97", 149 | "spender": "0xc92e8bdf79f0507f65a392b0ab4667716bfe0110", 150 | "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935", 151 | "nonce": "0", 152 | "deadline": "1872873982" 153 | } 154 | }`; 155 | const request = { 156 | method: "eth_signTypedData_v4", 157 | params: [from, jsonStr], 158 | }; 159 | 160 | const { evmMessage, hashToSign } = await requestRouter({ 161 | method: "eth_signTypedData_v4", 162 | chainId, 163 | params: [from, jsonStr], 164 | }); 165 | expect(evmMessage).toEqual(request.params[1]); 166 | expect(hashToSign).toEqual( 167 | "0x0c30e4ed702eb8688352a85513fa7e3590f9c5275dda0c382aa1d47ab0c5c99a" 168 | ); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /examples/abis/ERC1155.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "account", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "operator", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "bool", 20 | "name": "approved", 21 | "type": "bool" 22 | } 23 | ], 24 | "name": "ApprovalForAll", 25 | "type": "event" 26 | }, 27 | { 28 | "anonymous": false, 29 | "inputs": [ 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "operator", 34 | "type": "address" 35 | }, 36 | { 37 | "indexed": true, 38 | "internalType": "address", 39 | "name": "from", 40 | "type": "address" 41 | }, 42 | { 43 | "indexed": true, 44 | "internalType": "address", 45 | "name": "to", 46 | "type": "address" 47 | }, 48 | { 49 | "indexed": false, 50 | "internalType": "uint256[]", 51 | "name": "ids", 52 | "type": "uint256[]" 53 | }, 54 | { 55 | "indexed": false, 56 | "internalType": "uint256[]", 57 | "name": "values", 58 | "type": "uint256[]" 59 | } 60 | ], 61 | "name": "TransferBatch", 62 | "type": "event" 63 | }, 64 | { 65 | "anonymous": false, 66 | "inputs": [ 67 | { 68 | "indexed": true, 69 | "internalType": "address", 70 | "name": "operator", 71 | "type": "address" 72 | }, 73 | { 74 | "indexed": true, 75 | "internalType": "address", 76 | "name": "from", 77 | "type": "address" 78 | }, 79 | { 80 | "indexed": true, 81 | "internalType": "address", 82 | "name": "to", 83 | "type": "address" 84 | }, 85 | { 86 | "indexed": false, 87 | "internalType": "uint256", 88 | "name": "id", 89 | "type": "uint256" 90 | }, 91 | { 92 | "indexed": false, 93 | "internalType": "uint256", 94 | "name": "value", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "TransferSingle", 99 | "type": "event" 100 | }, 101 | { 102 | "anonymous": false, 103 | "inputs": [ 104 | { 105 | "indexed": false, 106 | "internalType": "string", 107 | "name": "value", 108 | "type": "string" 109 | }, 110 | { 111 | "indexed": true, 112 | "internalType": "uint256", 113 | "name": "id", 114 | "type": "uint256" 115 | } 116 | ], 117 | "name": "URI", 118 | "type": "event" 119 | }, 120 | { 121 | "inputs": [ 122 | { 123 | "internalType": "address", 124 | "name": "account", 125 | "type": "address" 126 | }, 127 | { 128 | "internalType": "uint256", 129 | "name": "id", 130 | "type": "uint256" 131 | } 132 | ], 133 | "name": "balanceOf", 134 | "outputs": [ 135 | { 136 | "internalType": "uint256", 137 | "name": "", 138 | "type": "uint256" 139 | } 140 | ], 141 | "stateMutability": "view", 142 | "type": "function" 143 | }, 144 | { 145 | "inputs": [ 146 | { 147 | "internalType": "address[]", 148 | "name": "accounts", 149 | "type": "address[]" 150 | }, 151 | { 152 | "internalType": "uint256[]", 153 | "name": "ids", 154 | "type": "uint256[]" 155 | } 156 | ], 157 | "name": "balanceOfBatch", 158 | "outputs": [ 159 | { 160 | "internalType": "uint256[]", 161 | "name": "", 162 | "type": "uint256[]" 163 | } 164 | ], 165 | "stateMutability": "view", 166 | "type": "function" 167 | }, 168 | { 169 | "inputs": [ 170 | { 171 | "internalType": "address", 172 | "name": "account", 173 | "type": "address" 174 | }, 175 | { 176 | "internalType": "address", 177 | "name": "operator", 178 | "type": "address" 179 | } 180 | ], 181 | "name": "isApprovedForAll", 182 | "outputs": [ 183 | { 184 | "internalType": "bool", 185 | "name": "", 186 | "type": "bool" 187 | } 188 | ], 189 | "stateMutability": "view", 190 | "type": "function" 191 | }, 192 | { 193 | "inputs": [ 194 | { 195 | "internalType": "address", 196 | "name": "from", 197 | "type": "address" 198 | }, 199 | { 200 | "internalType": "address", 201 | "name": "to", 202 | "type": "address" 203 | }, 204 | { 205 | "internalType": "uint256[]", 206 | "name": "ids", 207 | "type": "uint256[]" 208 | }, 209 | { 210 | "internalType": "uint256[]", 211 | "name": "amounts", 212 | "type": "uint256[]" 213 | }, 214 | { 215 | "internalType": "bytes", 216 | "name": "data", 217 | "type": "bytes" 218 | } 219 | ], 220 | "name": "safeBatchTransferFrom", 221 | "outputs": [], 222 | "stateMutability": "nonpayable", 223 | "type": "function" 224 | }, 225 | { 226 | "inputs": [ 227 | { 228 | "internalType": "address", 229 | "name": "from", 230 | "type": "address" 231 | }, 232 | { 233 | "internalType": "address", 234 | "name": "to", 235 | "type": "address" 236 | }, 237 | { 238 | "internalType": "uint256", 239 | "name": "id", 240 | "type": "uint256" 241 | }, 242 | { 243 | "internalType": "uint256", 244 | "name": "amount", 245 | "type": "uint256" 246 | }, 247 | { 248 | "internalType": "bytes", 249 | "name": "data", 250 | "type": "bytes" 251 | } 252 | ], 253 | "name": "safeTransferFrom", 254 | "outputs": [], 255 | "stateMutability": "nonpayable", 256 | "type": "function" 257 | }, 258 | { 259 | "inputs": [ 260 | { 261 | "internalType": "address", 262 | "name": "operator", 263 | "type": "address" 264 | }, 265 | { 266 | "internalType": "bool", 267 | "name": "approved", 268 | "type": "bool" 269 | } 270 | ], 271 | "name": "setApprovalForAll", 272 | "outputs": [], 273 | "stateMutability": "nonpayable", 274 | "type": "function" 275 | }, 276 | { 277 | "inputs": [ 278 | { 279 | "internalType": "bytes4", 280 | "name": "interfaceId", 281 | "type": "bytes4" 282 | } 283 | ], 284 | "name": "supportsInterface", 285 | "outputs": [ 286 | { 287 | "internalType": "bool", 288 | "name": "", 289 | "type": "bool" 290 | } 291 | ], 292 | "stateMutability": "view", 293 | "type": "function" 294 | }, 295 | { 296 | "inputs": [ 297 | { 298 | "internalType": "uint256", 299 | "name": "id", 300 | "type": "uint256" 301 | } 302 | ], 303 | "name": "uri", 304 | "outputs": [ 305 | { 306 | "internalType": "string", 307 | "name": "", 308 | "type": "string" 309 | } 310 | ], 311 | "stateMutability": "view", 312 | "type": "function" 313 | } 314 | ] -------------------------------------------------------------------------------- /src/chains/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | Hex, 4 | Hash, 5 | serializeTransaction, 6 | hashMessage, 7 | SignableMessage, 8 | serializeSignature, 9 | hashTypedData, 10 | TypedData, 11 | TypedDataDefinition, 12 | } from "viem"; 13 | import { 14 | BaseTx, 15 | AdapterParams, 16 | FunctionCallTransaction, 17 | TxPayload, 18 | SignArgs, 19 | Network, 20 | buildTxPayload, 21 | populateTx, 22 | toPayload, 23 | broadcastSignedTransaction, 24 | SignRequestData, 25 | IMpcContract, 26 | NearEncodedSignRequest, 27 | } from ".."; 28 | import { requestRouter } from "../utils"; 29 | 30 | /** 31 | * Adapter class for interacting with Ethereum through NEAR MPC contract 32 | */ 33 | export class NearEthAdapter { 34 | readonly mpcContract: IMpcContract; 35 | readonly address: Address; 36 | readonly derivationPath: string; 37 | 38 | private constructor(config: { 39 | mpcContract: IMpcContract; 40 | derivationPath: string; 41 | sender: Address; 42 | }) { 43 | this.mpcContract = config.mpcContract; 44 | this.derivationPath = config.derivationPath; 45 | this.address = config.sender; 46 | } 47 | 48 | /** 49 | * Gets the NEAR account ID linked to derived EVM account 50 | * 51 | * @returns The connected NEAR account ID 52 | */ 53 | nearAccountId(): string { 54 | return this.mpcContract.connectedAccount.accountId; 55 | } 56 | 57 | /** 58 | * Retrieves the balance of the Ethereum address associated with this adapter 59 | * 60 | * @param chainId - The chain ID of the Ethereum network to query 61 | * @returns The balance of the address in wei 62 | */ 63 | async getBalance(chainId: number): Promise { 64 | const network = Network.fromChainId(chainId); 65 | return network.client.getBalance({ address: this.address }); 66 | } 67 | 68 | /** 69 | * Constructs an EVM instance with the provided configuration 70 | * 71 | * @param args - The configuration object for the Adapter instance 72 | * @returns A new NearEthAdapter instance 73 | */ 74 | static async fromConfig(args: AdapterParams): Promise { 75 | const mpcContract = args.mpcContract; 76 | const derivationPath = args.derivationPath || "ethereum,1"; 77 | return new NearEthAdapter({ 78 | sender: await mpcContract.deriveEthAddress(derivationPath), 79 | derivationPath, 80 | mpcContract, 81 | }); 82 | } 83 | 84 | /** 85 | * Takes a minimally declared Ethereum Transaction, 86 | * builds the full transaction payload (with gas estimates, prices etc...), 87 | * acquires signature from Near MPC Contract and submits transaction to public mempool 88 | * 89 | * @param txData - Minimal transaction data to be signed by Near MPC and executed on EVM 90 | * @param nearGas - Manually specified gas to be sent with signature request 91 | * @returns The ethereum transaction hash 92 | */ 93 | async signAndSendTransaction( 94 | txData: BaseTx, 95 | nearGas?: bigint 96 | ): Promise { 97 | const { transaction, signArgs } = await this.createTxPayload(txData); 98 | const signature = await this.mpcContract.requestSignature( 99 | signArgs, 100 | nearGas 101 | ); 102 | return broadcastSignedTransaction({ transaction, signature }); 103 | } 104 | 105 | /** 106 | * Takes a minimally declared Ethereum Transaction, 107 | * builds the full transaction payload (with gas estimates, prices etc...), 108 | * and prepares the signature request payload for the Near MPC Contract 109 | * 110 | * @param txData - Minimal transaction data to be signed by Near MPC and executed on EVM 111 | * @param nearGas - Manually specified gas to be sent with signature request 112 | * @returns The transaction and request payload 113 | */ 114 | async getSignatureRequestPayload( 115 | txData: BaseTx, 116 | nearGas?: bigint 117 | ): Promise<{ 118 | transaction: Hex; 119 | requestPayload: FunctionCallTransaction<{ request: SignArgs }>; 120 | }> { 121 | const { transaction, signArgs } = await this.createTxPayload(txData); 122 | return { 123 | transaction, 124 | requestPayload: await this.mpcContract.encodeSignatureRequestTx( 125 | signArgs, 126 | nearGas 127 | ), 128 | }; 129 | } 130 | 131 | /** 132 | * Builds a Near Transaction Payload for Signing serialized EVM Transaction 133 | * 134 | * @param transaction - RLP encoded (i.e. serialized) Ethereum Transaction 135 | * @param nearGas - Optional gas parameter 136 | * @returns Prepared Near Transaction with signerId as this.address 137 | */ 138 | async mpcSignRequest( 139 | transaction: Hex, 140 | nearGas?: bigint 141 | ): Promise> { 142 | return this.mpcContract.encodeSignatureRequestTx( 143 | { 144 | payload: buildTxPayload(transaction), 145 | path: this.derivationPath, 146 | key_version: 0, 147 | }, 148 | nearGas 149 | ); 150 | } 151 | 152 | /** 153 | * Builds a complete, unsigned transaction (with nonce, gas estimates, current prices) 154 | * and payload bytes in preparation to be relayed to Near MPC contract 155 | * 156 | * @param tx - Minimal transaction data to be signed by Near MPC and executed on EVM 157 | * @returns Transaction and its bytes (the payload to be signed on Near) 158 | */ 159 | async createTxPayload(tx: BaseTx): Promise { 160 | const transaction = await this.buildTransaction(tx); 161 | const signArgs = { 162 | payload: buildTxPayload(transaction), 163 | path: this.derivationPath, 164 | key_version: 0, 165 | }; 166 | return { transaction, signArgs }; 167 | } 168 | 169 | /** 170 | * Transforms minimal transaction request data into a fully populated EVM transaction 171 | * 172 | * @param tx - Minimal transaction request data 173 | * @returns Serialized (aka RLP encoded) transaction 174 | */ 175 | async buildTransaction(tx: BaseTx): Promise { 176 | const transaction = await populateTx(tx, this.address); 177 | return serializeTransaction(transaction); 178 | } 179 | 180 | /** 181 | * Signs typed data according to EIP-712 182 | * 183 | * @param typedData - The typed data to sign 184 | * @returns The signature hash 185 | */ 186 | async signTypedData< 187 | const typedData extends TypedData | Record, 188 | primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, 189 | >(typedData: TypedDataDefinition): Promise { 190 | return this.sign(hashTypedData(typedData)); 191 | } 192 | 193 | /** 194 | * Signs a message according to personal_sign 195 | * 196 | * @param message - The message to sign 197 | * @returns The signature hash 198 | */ 199 | async signMessage(message: SignableMessage): Promise { 200 | return this.sign(hashMessage(message)); 201 | } 202 | 203 | /** 204 | * Requests signature from Near MPC Contract 205 | * 206 | * @param msgHash - Message Hash to be signed 207 | * @returns Two different potential signatures for the hash (one of which is valid) 208 | */ 209 | async sign(msgHash: `0x${string}` | Uint8Array): Promise { 210 | const signature = await this.mpcContract.requestSignature({ 211 | path: this.derivationPath, 212 | payload: toPayload(msgHash), 213 | key_version: 0, 214 | }); 215 | return serializeSignature(signature); 216 | } 217 | 218 | /** 219 | * Encodes a signature request for submission to the Near-Ethereum transaction MPC contract 220 | * 221 | * @param signRequest - The signature request data containing method, chain ID, and parameters 222 | * @returns The encoded Near-Ethereum transaction data, original EVM message, and recovery data 223 | */ 224 | async encodeSignRequest( 225 | signRequest: SignRequestData 226 | ): Promise { 227 | const { evmMessage, hashToSign } = await requestRouter(signRequest); 228 | return { 229 | nearPayload: await this.mpcContract.encodeSignatureRequestTx({ 230 | path: this.derivationPath, 231 | payload: toPayload(hashToSign), 232 | key_version: 0, 233 | }), 234 | evmMessage, 235 | hashToSign, 236 | }; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/types/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair } from "near-api-js"; 2 | import { IMpcContract } from "../mpcContract"; 3 | import { 4 | Address, 5 | Hash, 6 | Hex, 7 | Signature, 8 | TransactionSerializable, 9 | TypedDataDomain, 10 | } from "viem"; 11 | import { NearConfig } from "near-api-js/lib/near"; 12 | 13 | /** 14 | * Borrowed from \@near-wallet-selector/core 15 | * {@link https://github.com/near/wallet-selector/blob/01081aefaa3c96ded9f83a23ecf0f210a4b64590/packages/core/src/lib/wallet/transactions.types.ts#L12} 16 | */ 17 | export interface FunctionCallAction { 18 | type: "FunctionCall"; 19 | params: { 20 | methodName: string; 21 | args: T; 22 | gas: string; 23 | deposit: string; 24 | }; 25 | } 26 | 27 | /** Configuration for a NEAR account */ 28 | export interface NearAccountConfig { 29 | /** The key pair associated with the account */ 30 | keyPair: KeyPair; 31 | /** The NEAR account ID */ 32 | accountId: string; 33 | /** Network settings */ 34 | network: NearConfig; 35 | } 36 | 37 | /** Base transaction structure */ 38 | export interface BaseTx { 39 | /** Recipient of the transaction */ 40 | to: `0x${string}`; 41 | /** ETH value of the transaction */ 42 | value?: bigint; 43 | /** Call data of the transaction */ 44 | data?: `0x${string}`; 45 | /** Integer ID of the network for the transaction */ 46 | chainId: number; 47 | /** Specified transaction nonce */ 48 | nonce?: number; 49 | /** Optional gas limit */ 50 | gas?: bigint; 51 | } 52 | 53 | /** Parameters for the adapter */ 54 | export interface AdapterParams { 55 | /** An instance of the NearMPC contract connected to the associated NEAR account */ 56 | mpcContract: IMpcContract; 57 | /** Path used to generate ETH account from NEAR account (e.g., "ethereum,1") */ 58 | derivationPath?: string; 59 | } 60 | 61 | /** 62 | * Represents a message that can be signed within an Ethereum Virtual Machine (EVM) context. 63 | * This can be a raw string, an EIP-712 typed data structure, or a serializable transaction. 64 | */ 65 | export type EvmMessage = string | EIP712TypedData | TransactionSerializable; 66 | 67 | /** Encapsulates a signature request for an Ethereum-based message */ 68 | export interface EncodedSignRequest { 69 | /** The message to be signed, which could be in plain string format, 70 | * an EIP-712 typed data, or a serializable transaction */ 71 | evmMessage: EvmMessage; 72 | /** A unique hash derived from `evmMessage` to identify the signature request */ 73 | hashToSign: Hash; 74 | } 75 | 76 | /** 77 | * Extends the `EncodedSignRequest` for use with NEAR protocol. 78 | * This structure contains an additional payload to facilitate transaction signing in NEAR. 79 | */ 80 | export interface NearEncodedSignRequest extends EncodedSignRequest { 81 | /** A NEAR-specific transaction payload, typically including a request with arguments 82 | * for the function call */ 83 | nearPayload: FunctionCallTransaction<{ 84 | request: SignArgs; 85 | }>; 86 | } 87 | 88 | /** 89 | * Arguments required for signature request from MPC Contract. 90 | * {@link https://github.com/near/mpc/blob/48a572baab5904afe3cd62bd0da5a036db3a34b6/chain-signatures/contract/src/primitives.rs#L268} 91 | */ 92 | export interface SignArgs { 93 | /** Derivation path for ETH account associated with NEAR AccountId */ 94 | path: string; 95 | /** Serialized Ethereum transaction bytes */ 96 | payload: number[]; 97 | /** Version number associated with derived ETH address (must be increasing) */ 98 | key_version: number; 99 | } 100 | 101 | /** Represents the payload for a transaction */ 102 | export interface TxPayload { 103 | /** Serialized Ethereum transaction */ 104 | transaction: Hex; 105 | /** Arguments required by NEAR MPC Contract signature request */ 106 | signArgs: SignArgs; 107 | } 108 | 109 | /** Represents a function call transaction */ 110 | export interface FunctionCallTransaction { 111 | /** Signer of the function call */ 112 | signerId: string; 113 | /** Transaction recipient (a NEAR ContractId) */ 114 | receiverId: string; 115 | /** Function call actions */ 116 | actions: Array>; 117 | } 118 | 119 | /** 120 | * Result Type of MPC contract signature request. 121 | * Representing Affine Points on elliptic curve. 122 | * Example: 123 | * ```json 124 | * { 125 | * "big_r": { 126 | * "affine_point": "031F2CE94AF69DF45EC96D146DB2F6D35B8743FA2E21D2450070C5C339A4CD418B" 127 | * }, 128 | * "s": { 129 | * "scalar": "5AE93A7C4138972B3FE8AEA1638190905C6DB5437BDE7274BEBFA41DDAF7E4F6" 130 | * }, 131 | * "recovery_id": 0 132 | * } 133 | * ``` 134 | */ 135 | export interface MPCSignature { 136 | /** The R point of the signature */ 137 | big_r: { affine_point: string }; 138 | /** The S value of the signature */ 139 | s: { scalar: string }; 140 | /** The recovery ID */ 141 | recovery_id: number; 142 | } 143 | 144 | export interface TypedDataTypes { 145 | name: string; 146 | type: string; 147 | } 148 | 149 | export type TypedMessageTypes = { 150 | [key: string]: TypedDataTypes[]; 151 | }; 152 | 153 | /** Represents the data for a typed message */ 154 | export type EIP712TypedData = { 155 | /** The domain of the message */ 156 | domain: TypedDataDomain; 157 | /** The types of the message */ 158 | types: TypedMessageTypes; 159 | /** The message itself */ 160 | message: Record; 161 | /** The primary type of the message */ 162 | primaryType: string; 163 | }; 164 | 165 | /** Sufficient data required to construct a signed Ethereum Transaction */ 166 | export interface TransactionWithSignature { 167 | /** Unsigned Ethereum transaction data */ 168 | transaction: Hex; 169 | /** Representation of the transaction's signature */ 170 | signature: Signature; 171 | } 172 | 173 | /** Interface representing the parameters required for an Ethereum transaction */ 174 | export interface EthTransactionParams { 175 | /** The sender's Ethereum address in hexadecimal format */ 176 | from: Hex; 177 | /** The recipient's Ethereum address in hexadecimal format */ 178 | to: Hex; 179 | /** Optional gas limit for the transaction in hexadecimal format */ 180 | gas?: Hex; 181 | /** Optional amount of Ether to send in hexadecimal format */ 182 | value?: Hex; 183 | /** Optional data payload for the transaction in hexadecimal format, often used for contract interactions */ 184 | data?: Hex; 185 | } 186 | 187 | /** 188 | * Parameters for a personal_sign request 189 | * Tuple of [message: Hex, signer: Address] 190 | */ 191 | export type PersonalSignParams = [Hex, Address]; 192 | 193 | /** 194 | * Parameters for an eth_sign request 195 | * Tuple of [signer: Address, message: Hex] 196 | */ 197 | export type EthSignParams = [Address, Hex]; 198 | 199 | /** 200 | * Parameters for signing complex structured data (like EIP-712) 201 | * Tuple of [signer: Hex, structuredData: string] 202 | */ 203 | export type TypedDataParams = [Hex, string]; 204 | 205 | /** Type representing the possible request parameters for a signing session */ 206 | export type SessionRequestParams = 207 | | EthTransactionParams[] 208 | | Hex 209 | | PersonalSignParams 210 | | EthSignParams 211 | | TypedDataParams; 212 | 213 | /** An array of supported signing methods */ 214 | export const signMethods = [ 215 | "eth_sign", 216 | "personal_sign", 217 | "eth_sendTransaction", 218 | "eth_signTypedData", 219 | "eth_signTypedData_v4", 220 | ] as const; 221 | 222 | /** Type representing one of the supported signing methods */ 223 | export type SignMethod = (typeof signMethods)[number]; 224 | 225 | /** Interface representing the data required for a signature request */ 226 | export type SignRequestData = { 227 | /** The signing method to be used */ 228 | method: SignMethod; 229 | /** The ID of the Ethereum chain where the transaction or signing is taking place */ 230 | chainId: number; 231 | /** The parameters required for the signing request, which vary depending on the method */ 232 | params: SessionRequestParams; 233 | }; 234 | /** Template literal type for NEAR key pair strings */ 235 | export type KeyPairString = `ed25519:${string}` | `secp256k1:${string}`; 236 | 237 | /** 238 | * Configuration for setting up the adapter 239 | */ 240 | export interface SetupConfig { 241 | /** The NEAR account ID */ 242 | accountId: string; 243 | /** The MPC contract ID */ 244 | mpcContractId: string; 245 | /** The NEAR network configuration */ 246 | network?: NearConfig; 247 | /** The private key for the account */ 248 | privateKey?: string; 249 | /** The derivation path for the Ethereum account. Defaults to "ethereum,1" */ 250 | derivationPath?: string; 251 | /** The root public key for the account. If not available it will be fetched from the MPC contract */ 252 | rootPublicKey?: string; 253 | } 254 | -------------------------------------------------------------------------------- /src/network/constants.ts: -------------------------------------------------------------------------------- 1 | interface ChainInfo { 2 | currencyIcon?: string; 3 | icon?: string; 4 | wrappedToken: string; 5 | } 6 | 7 | /// A short list of networks with known wrapped tokens. 8 | 9 | const DATA_IMG = "data:image/svg+xml;base64,"; 10 | 11 | // Native Assets 12 | const ETHER = `${DATA_IMG}PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjYgNiA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfOTExNV80MDk4NSkiPg0KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMjQiIGZpbGw9IiM2NzgwRTMiLz4NCjwvZz4NCjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF85MTE1XzQwOTg1KSI+DQo8cGF0aCBkPSJNMzAuNDk4MiAxNUwzMC4zMTI1IDE1LjY2MDlWMzQuODM2M0wzMC40OTgyIDM1LjAzMDRMMzguOTk1NSAyOS43NjlMMzAuNDk4MiAxNVoiIGZpbGw9IiNDMkNCRjMiLz4NCjxwYXRoIGQ9Ik0zMC40OTc1IDE1TDIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDRWMjUuNzIzMlYxNVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNDk5MiAzNi43MTU3TDMwLjM5NDUgMzYuODQ5NFY0My42Nzk5TDMwLjQ5OTIgNDQuMDAwMUwzOS4wMDE3IDMxLjQ1N0wzMC40OTkyIDM2LjcxNTdaIiBmaWxsPSIjQzBDQUYyIi8+DQo8cGF0aCBkPSJNMzAuNDk3NSA0NC4wMDAxVjM2LjcxNTdMMjIgMzEuNDU3TDMwLjQ5NzUgNDQuMDAwMVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNSAzNS4wMzAzTDM4Ljk5NzMgMjkuNzY5TDMwLjUgMjUuNzIzMVYzNS4wMzAzWiIgZmlsbD0iIzg1OThFOCIvPg0KPHBhdGggZD0iTTIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDNWMjUuNzIzMUwyMiAyOS43NjlaIiBmaWxsPSIjQzJDQkYzIi8+DQo8L2c+DQo8ZGVmcz4NCjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZF85MTE1XzQwOTg1IiB4PSIwIiB5PSIwIiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4NCjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+DQo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4NCjxmZU9mZnNldC8+DQo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIzIi8+DQo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4NCjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA3IDAiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiIHJlc3VsdD0ic2hhcGUiLz4NCjwvZmlsdGVyPg0KPGNsaXBQYXRoIGlkPSJjbGlwMF85MTE1XzQwOTg1Ij4NCjxyZWN0IHdpZHRoPSIxNyIgaGVpZ2h0PSIyOSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyIDE1KSIvPg0KPC9jbGlwUGF0aD4NCjwvZGVmcz4NCjwvc3ZnPg0K`; 13 | const XDAI = `${DATA_IMG}PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjM3NHB4IiBoZWlnaHQ9IjM3NHB4IiBzdHlsZT0ic2hhcGUtcmVuZGVyaW5nOmdlb21ldHJpY1ByZWNpc2lvbjsgdGV4dC1yZW5kZXJpbmc6Z2VvbWV0cmljUHJlY2lzaW9uOyBpbWFnZS1yZW5kZXJpbmc6b3B0aW1pemVRdWFsaXR5OyBmaWxsLXJ1bGU6ZXZlbm9kZDsgY2xpcC1ydWxlOmV2ZW5vZGQiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGc+PHBhdGggc3R5bGU9Im9wYWNpdHk6MSIgZmlsbD0iIzAwMzM3MCIgZD0iTSA0Mi41LC0wLjUgQyAxMzguNSwtMC41IDIzNC41LC0wLjUgMzMwLjUsLTAuNUMgMzUzLjE2Nyw1LjUgMzY3LjUsMTkuODMzMyAzNzMuNSw0Mi41QyAzNzMuNSwxMzguNSAzNzMuNSwyMzQuNSAzNzMuNSwzMzAuNUMgMzY3LjUsMzUzLjE2NyAzNTMuMTY3LDM2Ny41IDMzMC41LDM3My41QyAyMzQuNSwzNzMuNSAxMzguNSwzNzMuNSA0Mi41LDM3My41QyAxOS44MzMzLDM2Ny41IDUuNSwzNTMuMTY3IC0wLjUsMzMwLjVDIC0wLjUsMjM0LjUgLTAuNSwxMzguNSAtMC41LDQyLjVDIDUuNSwxOS44MzMzIDE5LjgzMzMsNS41IDQyLjUsLTAuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGFlMTNhIiBkPSJNIDE4Ny41LDE0My41IEMgMTk1LjgzMywxNTIuMTY3IDIwNC4xNjcsMTYwLjgzMyAyMTIuNSwxNjkuNUMgMjE5LjcxNCwxODIuMzI2IDIxOC44OCwxOTQuNjU5IDIxMCwyMDYuNUMgMjAyLjI3MiwyMTQuMzk2IDE5NC40MzgsMjIyLjA2MyAxODYuNSwyMjkuNUMgMTY5LjM2OCwyNDcuMTMyIDE1Mi4wMzUsMjY0LjYzMiAxMzQuNSwyODJDIDEyMS45MzIsMjkxLjE4MSAxMDguOTMyLDI5MS44NDggOTUuNSwyODRDIDgyLjU0MzEsMjcyLjQzNCA4MC4wNDMxLDI1OC45MzQgODgsMjQzLjVDIDEwNC4zMDQsMjI2LjE5NSAxMjAuOTcxLDIwOS4xOTUgMTM4LDE5Mi41QyAxNDAuNzg2LDE4OS4wMzMgMTQxLjEyLDE4NS4zNjYgMTM5LDE4MS41QyAxMjIuNjY3LDE2NS4xNjcgMTA2LjMzMywxNDguODMzIDkwLDEzMi41QyA3OS44MjQ5LDExNi40MDYgODEuNjU4MiwxMDEuOTA2IDk1LjUsODlDIDEwOC45Niw4MS4xMTk5IDEyMS45Niw4MS43ODY1IDEzNC41LDkxQyAxNTIuMDM1LDEwOC43MDIgMTY5LjcwMSwxMjYuMjAyIDE4Ny41LDE0My41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwYWUxM2EiIGQ9Ik0gMjc2LjUsMTM5LjUgQyAyNjAuMDA1LDE0Ny4wMzYgMjQ2LjE3MiwxNDMuNzAyIDIzNSwxMjkuNUMgMjI5LjMxMywxMTguODA5IDIyOS4xNDYsMTA4LjE0MyAyMzQuNSw5Ny41QyAyNDYuNjU1LDgyLjEzNyAyNjEuMzIyLDc5LjMwMzcgMjc4LjUsODlDIDI5MS45OTQsMTAxLjMyMiAyOTQuMTYxLDExNS40ODkgMjg1LDEzMS41QyAyODIuMjcxLDEzNC4zOTkgMjc5LjQzOCwxMzcuMDY2IDI3Ni41LDEzOS41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwNDc4NWIiIGQ9Ik0gMjM0LjUsOTcuNSBDIDIyOS4xNDYsMTA4LjE0MyAyMjkuMzEzLDExOC44MDkgMjM1LDEyOS41QyAyNDYuMTcyLDE0My43MDIgMjYwLjAwNSwxNDcuMDM2IDI3Ni41LDEzOS41QyAyNjIuODY5LDE1My42MzIgMjQ5LjAzNSwxNjcuNjMyIDIzNSwxODEuNUMgMjMzLjE4MiwxODQuNDE2IDIzMy4wMTYsMTg3LjQxNiAyMzQuNSwxOTAuNUMgMjI3LjE2NywxODMuNSAyMTkuODMzLDE3Ni41IDIxMi41LDE2OS41QyAyMDQuMTY3LDE2MC44MzMgMTk1LjgzMywxNTIuMTY3IDE4Ny41LDE0My41QyAyMDMuMTQsMTI4LjE5NCAyMTguODA3LDExMi44NjEgMjM0LjUsOTcuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGRiNDBiIiBkPSJNIDIxMi41LDE2OS41IEMgMjE5LjgzMywxNzYuNSAyMjcuMTY3LDE4My41IDIzNC41LDE5MC41QyAyNTEuMTMyLDIwNy42MzIgMjY3Ljk2NSwyMjQuNjMyIDI4NSwyNDEuNUMgMjk0LjA2NywyNTcuNDIgMjkxLjkwMSwyNzEuNTg3IDI3OC41LDI4NEMgMjY3LjA1NSwyOTAuOTM1IDI1NS4zODksMjkxLjI2OSAyNDMuNSwyODVDIDIyNC45MzYsMjY1LjkzNCAyMDUuOTM2LDI0Ny40MzQgMTg2LjUsMjI5LjVDIDE5NC40MzgsMjIyLjA2MyAyMDIuMjcyLDIxNC4zOTYgMjEwLDIwNi41QyAyMTguODgsMTk0LjY1OSAyMTkuNzE0LDE4Mi4zMjYgMjEyLjUsMTY5LjUgWiIvPjwvZz4KPC9zdmc+Cg==`; 14 | const POL = `${DATA_IMG}PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMyIgZmlsbD0iIzZDMDBGNiIvPgo8cGF0aCBkPSJNMTAuMTY3NCA0TDcuMzYxNzUgNS42MTQzOFYxMC42NTI5TDUuODEzNzEgMTEuNTUxOUw0LjI1NjE4IDEwLjY1MjFWOC44NTMyNkw1LjgxMzcxIDcuOTYyMjVMNi44MTUxNiA4LjU0NDFWNy4wODg3TDUuODA0OTQgNi41MTQxMkwzIDguMTQ2NzRWMTEuMzc2Mkw1LjgxNDQzIDEzTDguNjE5MzUgMTEuMzc2MlY2LjMzODQyTDEwLjE3NjkgNS40Mzg2MkwxMS43MzM2IDYuMzM4NDJWOC4xMjkyM0wxMC4xNzY5IDkuMDM3MDNMOS4xNjY2NiA4LjQ1MDAzVjkuODk4MTZMMTAuMTY3NCAxMC40NzY0TDEzIDguODYyMDVWNS42MTQzOEwxMC4xNjc0IDRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K`; 15 | const AVAX_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMTUwMyIgaGVpZ2h0PSIxNTA0IiB2aWV3Qm94PSIwIDAgMTUwMyAxNTA0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB4PSIyODciIHk9IjI1OCIgd2lkdGg9IjkyOCIgaGVpZ2h0PSI4NDQiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTUwMi41IDc1MkMxNTAyLjUgMTE2Ni43NyAxMTY2LjI3IDE1MDMgNzUxLjUgMTUwM0MzMzYuNzM0IDE1MDMgMC41IDExNjYuNzcgMC41IDc1MkMwLjUgMzM3LjIzNCAzMzYuNzM0IDEgNzUxLjUgMUMxMTY2LjI3IDEgMTUwMi41IDMzNy4yMzQgMTUwMi41IDc1MlpNNTM4LjY4OCAxMDUwLjg2SDM5Mi45NEMzNjIuMzE0IDEwNTAuODYgMzQ3LjE4NiAxMDUwLjg2IDMzNy45NjIgMTA0NC45NkMzMjcuOTk5IDEwMzguNSAzMjEuOTExIDEwMjcuOCAzMjEuMTczIDEwMTUuOTlDMzIwLjYxOSAxMDA1LjExIDMyOC4xODQgOTkxLjgyMiAzNDMuMzEyIDk2NS4yNTVMNzAzLjE4MiAzMzAuOTM1QzcxOC40OTUgMzAzLjk5OSA3MjYuMjQzIDI5MC41MzEgNzM2LjAyMSAyODUuNTVDNzQ2LjUzNyAyODAuMiA3NTkuMDgzIDI4MC4yIDc2OS41OTkgMjg1LjU1Qzc3OS4zNzcgMjkwLjUzMSA3ODcuMTI2IDMwMy45OTkgODAyLjQzOCAzMzAuOTM1TDg3Ni40MiA0NjAuMDc5TDg3Ni43OTcgNDYwLjczOEM4OTMuMzM2IDQ4OS42MzUgOTAxLjcyMyA1MDQuMjg5IDkwNS4zODUgNTE5LjY2OUM5MDkuNDQzIDUzNi40NTggOTA5LjQ0MyA1NTQuMTY5IDkwNS4zODUgNTcwLjk1OEM5MDEuNjk1IDU4Ni40NTUgODkzLjM5MyA2MDEuMjE1IDg3Ni42MDQgNjMwLjU0OUw2ODcuNTczIDk2NC43MDJMNjg3LjA4NCA5NjUuNTU4QzY3MC40MzYgOTk0LjY5MyA2NjEuOTk5IDEwMDkuNDYgNjUwLjMwNiAxMDIwLjZDNjM3LjU3NiAxMDMyLjc4IDYyMi4yNjMgMTA0MS42MyA2MDUuNDc0IDEwNDYuNjJDNTkwLjE2MSAxMDUwLjg2IDU3My4wMDQgMTA1MC44NiA1MzguNjg4IDEwNTAuODZaTTkwNi43NSAxMDUwLjg2SDExMTUuNTlDMTE0Ni40IDEwNTAuODYgMTE2MS45IDEwNTAuODYgMTE3MS4xMyAxMDQ0Ljc4QzExODEuMDkgMTAzOC4zMiAxMTg3LjM2IDEwMjcuNDMgMTE4Ny45MiAxMDE1LjYzQzExODguNDUgMTAwNS4xIDExODEuMDUgOTkyLjMzIDExNjYuNTUgOTY3LjMwN0MxMTY2LjA1IDk2Ni40NTUgMTE2NS41NSA5NjUuNTg4IDExNjUuMDQgOTY0LjcwNkwxMDYwLjQzIDc4NS43NUwxMDU5LjI0IDc4My43MzVDMTA0NC41NCA3NTguODc3IDEwMzcuMTIgNzQ2LjMyNCAxMDI3LjU5IDc0MS40NzJDMTAxNy4wOCA3MzYuMTIxIDEwMDQuNzEgNzM2LjEyMSA5OTQuMTk5IDc0MS40NzJDOTg0LjYwNSA3NDYuNDUzIDk3Ni44NTcgNzU5LjU1MiA5NjEuNTQ0IDc4NS45MzRMODU3LjMwNiA5NjQuODkxTDg1Ni45NDkgOTY1LjUwN0M4NDEuNjkgOTkxLjg0NyA4MzQuMDY0IDEwMDUuMDEgODM0LjYxNCAxMDE1LjgxQzgzNS4zNTIgMTAyNy42MiA4NDEuNDQgMTAzOC41IDg1MS40MDIgMTA0NC45NkM4NjAuNDQzIDEwNTAuODYgODc1Ljk0IDEwNTAuODYgOTA2Ljc1IDEwNTAuODZaIiBmaWxsPSIjRTg0MTQyIi8+Cjwvc3ZnPgo=`; 16 | 17 | // Network Logos 18 | const ETHEREUM_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAxNiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxyZWN0IHk9IjAuNSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMyIgZmlsbD0iIzY3ODBFMyIvPg0KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzkxMTVfNDA5NjQpIj4NCjxwYXRoIGQ9Ik04LjE2NjA4IDIuNUw4LjA4OTYgMi43NzIxM1YxMC42Njc5TDguMTY2MDggMTAuNzQ3OEwxMS42NjUgOC41ODEzN0w4LjE2NjA4IDIuNVoiIGZpbGw9IiNDMkNCRjMiLz4NCjxwYXRoIGQ9Ik04LjE2NTcyIDIuNUw0LjY2Njc1IDguNTgxMzdMOC4xNjU3MiAxMC43NDc4VjYuOTE1NDNWMi41WiIgZmlsbD0id2hpdGUiLz4NCjxwYXRoIGQ9Ik04LjE2NjQgMTEuNDQxN0w4LjEyMzI5IDExLjQ5NjdWMTQuMzA5M0w4LjE2NjQgMTQuNDQxMUwxMS42Njc0IDkuMjc2MzdMOC4xNjY0IDExLjQ0MTdaIiBmaWxsPSIjQzBDQUYyIi8+DQo8cGF0aCBkPSJNOC4xNjU3MiAxNC40NDExVjExLjQ0MTdMNC42NjY3NSA5LjI3NjM3TDguMTY1NzIgMTQuNDQxMVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNOC4xNjY3NSAxMC43NDc5TDExLjY2NTYgOC41ODE0N0w4LjE2Njc1IDYuOTE1NTNWMTAuNzQ3OVoiIGZpbGw9IiM4NTk4RTgiLz4NCjxwYXRoIGQ9Ik00LjY2Njc1IDguNTgxNDdMOC4xNjU3MiAxMC43NDc5VjYuOTE1NTNMNC42NjY3NSA4LjU4MTQ3WiIgZmlsbD0iI0MyQ0JGMyIvPg0KPC9nPg0KPGRlZnM+DQo8Y2xpcFBhdGggaWQ9ImNsaXAwXzkxMTVfNDA5NjQiPg0KPHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMTEuOTQxMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuNjY2NzUgMi41KSIvPg0KPC9jbGlwUGF0aD4NCjwvZGVmcz4NCjwvc3ZnPg0K`; 19 | const GNOSIS_ICON = `${DATA_IMG}PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI2LjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA0MjggNDI4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjggNDI4OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMTI1LjgsMjQzLjdjMTIuMywwLDI0LjMtNC4xLDM0LTExLjZsLTc4LTc4Yy0xOC44LDI0LjMtMTQuMyw1OS4zLDEwLDc4LjEKCUMxMDEuNiwyMzkuNiwxMTMuNSwyNDMuNywxMjUuOCwyNDMuN0wxMjUuOCwyNDMuN3oiLz4KPHBhdGggc3R5bGU9ImZpbGw6IzAwMTkzQzsiIGQ9Ik0zNTcuOCwxODhjMC0xMi4zLTQuMS0yNC4zLTExLjYtMzRsLTc4LDc4YzI0LjMsMTguOCw1OS4yLDE0LjMsNzgtMTAKCUMzNTMuNywyMTIuMywzNTcuOCwyMDAuMywzNTcuOCwxODh6Ii8+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMzk3LjEsMTAzLjFsLTM0LjUsMzQuNWMyNy44LDMzLjMsMjMuNCw4Mi45LTkuOSwxMTAuN2MtMjkuMiwyNC40LTcxLjYsMjQuNC0xMDAuOCwwTDIxNCwyODYuMgoJbC0zNy44LTM3LjhjLTMzLjMsMjcuOC04Mi45LDIzLjQtMTEwLjctOS45Yy0yNC40LTI5LjItMjQuNC03MS42LDAtMTAwLjhMNDcuOCwxMjBMMzEsMTAzLjFDMTAuNywxMzYuNSwwLDE3NC45LDAsMjE0CgljMCwxMTguMiw5NS44LDIxNCwyMTQsMjE0czIxNC05NS44LDIxNC0yMTRDNDI4LjEsMTc0LjksNDE3LjMsMTM2LjUsMzk3LjEsMTAzLjF6Ii8+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMzY4LjgsNjYuM2MtODEuNS04NS41LTIxNi45LTg4LjctMzAyLjQtNy4yYy0yLjUsMi40LTQuOSw0LjgtNy4yLDcuMmMtNS4zLDUuNi0xMC4zLDExLjQtMTUsMTcuNQoJTDIxNCwyNTMuN0wzODMuOCw4My44QzM3OS4yLDc3LjcsMzc0LjEsNzEuOSwzNjguOCw2Ni4zeiBNMjE0LDI4YzUwLDAsOTYuNiwxOS4zLDEzMS42LDU0LjVMMjE0LDIxNC4xTDgyLjQsODIuNQoJQzExNy40LDQ3LjMsMTY0LDI4LDIxNCwyOHoiLz4KPC9zdmc+Cg==`; 20 | const ARBITRUM_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iMyIgZmlsbD0iIzJGMzc0OSIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfOTExNV80MTQyOCkiPgo8cGF0aCBkPSJNMy4zOTg0NCA3Ljg4NFYxNi4xMTZDMy4zOTg0NCAxNi42NDUyIDMuNjc1NjQgMTcuMTI0IDQuMTM3NjQgMTcuMzkyOEwxMS4yNjkyIDIxLjUwODhDMTEuNDkzMSAyMS42MzY4IDExLjc0NjQgMjEuNzA0MSAxMi4wMDQyIDIxLjcwNDFDMTIuMjYyMSAyMS43MDQxIDEyLjUxNTQgMjEuNjM2OCAxMi43MzkyIDIxLjUwODhMMTkuODcwOCAxNy4zOTI4QzIwLjA5NTMgMTcuMjYzNiAyMC4yODE4IDE3LjA3NzYgMjAuNDExNSAxNi44NTM1QzIwLjU0MTMgMTYuNjI5MyAyMC42MDk4IDE2LjM3NSAyMC42MSAxNi4xMTZWNy44ODRDMjAuNjEgNy4zNTQ4IDIwLjMzMjggNi44NzYgMTkuODcwOCA2LjYwNzJMMTIuNzM5MiAyLjQ5MTJDMTIuNTE1NCAyLjM2MzIyIDEyLjI2MjEgMi4yOTU5IDEyLjAwNDIgMi4yOTU5QzExLjc0NjQgMi4yOTU5IDExLjQ5MzEgMi4zNjMyMiAxMS4yNjkyIDIuNDkxMkw0LjEzNzY0IDYuNjA3MkMzLjY4NDA0IDYuODY3NiAzLjQwNjg0IDcuMzU0OCAzLjQwNjg0IDcuODg0SDMuMzk4NDRaIiBmaWxsPSIjMjEzMTQ3Ii8+CjxwYXRoIGQ9Ik0xMy41NTM5IDEzLjU5NjFMMTIuNTM3NSAxNi4zODQ5QzEyLjUxMjMgMTYuNDY0MSAxMi41MTIzIDE2LjU0OTIgMTIuNTM3NSAxNi42Mjg1TDE0LjI4NDcgMjEuNDI0OUwxNi4zMDkxIDIwLjI1NzNMMTMuODgxNSAxMy41OTYxQzEzLjg2ODkgMTMuNTYyNyAxMy44NDY1IDEzLjUzNCAxMy44MTczIDEzLjUxMzhDMTMuNzg4IDEzLjQ5MzUgMTMuNzUzMyAxMy40ODI3IDEzLjcxNzcgMTMuNDgyN0MxMy42ODIxIDEzLjQ4MjcgMTMuNjQ3MyAxMy40OTM1IDEzLjYxODEgMTMuNTEzOEMxMy41ODg4IDEzLjUzNCAxMy41NjY0IDEzLjU2MjcgMTMuNTUzOSAxMy41OTYxWiIgZmlsbD0iIzEyQUFGRiIvPgo8cGF0aCBkPSJNMTUuNTk1MyA4LjkwODgxQzE1LjU4MjggOC44NzU0OSAxNS41NjA0IDguODQ2NzkgMTUuNTMxMSA4LjgyNjUzQzE1LjUwMTkgOC44MDYyNyAxNS40NjcxIDguNzk1NDEgMTUuNDMxNSA4Ljc5NTQxQzE1LjM5NTkgOC43OTU0MSAxNS4zNjEyIDguODA2MjcgMTUuMzMxOSA4LjgyNjUzQzE1LjMwMjcgOC44NDY3OSAxNS4yODAzIDguODc1NDkgMTUuMjY3NyA4LjkwODgxTDE0LjI1MTMgMTEuNjk3NkMxNC4yMjYxIDExLjc3NjkgMTQuMjI2MSAxMS44NjIgMTQuMjUxMyAxMS45NDEyTDE3LjExNTcgMTkuNzk1MkwxOS4xNDAxIDE4LjYyNzZMMTUuNTk1MyA4LjkxNzIxVjguOTA4ODFaIiBmaWxsPSIjMTJBQUZGIi8+CjxwYXRoIGQ9Ik0xMi4wMDAxIDIuODAyMDFDMTIuMDUwNiAyLjgwMzAzIDEyLjA5OTkgMi44MTc1MyAxMi4xNDI5IDIuODQ0MDFMMTkuODU0MSA3LjI5NjAxQzE5Ljk0NjUgNy4zNDY0MSAxOS45OTY5IDcuNDQ3MjEgMTkuOTk2OSA3LjU0ODAxVjE2LjQ1MkMxOS45OTU5IDE2LjUwMjYgMTkuOTgyNCAxNi41NTIxIDE5Ljk1NzQgMTYuNTk2MUMxOS45MzI1IDE2LjY0MDEgMTkuODk3IDE2LjY3NzIgMTkuODU0MSAxNi43MDRMMTIuMTQyOSAyMS4xNTZDMTIuMTAwMyAyMS4xODM0IDEyLjA1MDcgMjEuMTk4IDEyLjAwMDEgMjEuMTk4QzExLjk0OTQgMjEuMTk4IDExLjg5OTkgMjEuMTgzNCAxMS44NTczIDIxLjE1Nkw0LjE0NjA3IDE2LjcwNEM0LjA1MzY3IDE2LjY1MzYgNC4wMDMyNyAxNi41NTI4IDQuMDAzMjcgMTYuNDUyVjcuNTM5NjFDNC4wMDQyMyA3LjQ4OTA0IDQuMDE3NzkgNy40Mzk1MSA0LjA0MjcyIDcuMzk1NTFDNC4wNjc2NiA3LjM1MTUgNC4xMDMxOCA3LjMxNDQyIDQuMTQ2MDcgNy4yODc2MUwxMS44NTczIDIuODM1NjFDMTEuOTAwMyAyLjgwOTEzIDExLjk0OTYgMi43OTQ2MyAxMi4wMDAxIDIuNzkzNjFWMi44MDIwMVpNMTIuMDAwMSAxLjUwMDAxQzExLjcyMDMgMS40OTg5MiAxMS40NDUxIDEuNTcxMzQgMTEuMjAyMSAxLjcxMDAxTDMuNDkwODcgNi4xNjIwMUMzLjI0ODEyIDYuMzAwNzkgMy4wNDY0MiA2LjUwMTM0IDIuOTA2MjYgNi43NDMyOUMyLjc2NjExIDYuOTg1MjUgMi42OTI0OCA3LjI1OTk5IDIuNjkyODcgNy41Mzk2MVYxNi40NDM2QzIuNjkyNDggMTYuNzIzMiAyLjc2NjExIDE2Ljk5OCAyLjkwNjI2IDE3LjIzOTlDMy4wNDY0MiAxNy40ODE5IDMuMjQ4MTIgMTcuNjgyNCAzLjQ5MDg3IDE3LjgyMTJMMTEuMjAyMSAyMi4yNzMyQzExLjQ0NTcgMjIuNDE2IDExLjcyMjkgMjIuNDgzMiAxMi4wMDAxIDIyLjQ4MzJDMTIuMjc5OSAyMi40ODQzIDEyLjU1NTEgMjIuNDExOSAxMi43OTgxIDIyLjI3MzJMMjAuNTA5MyAxNy44MjEyQzIwLjc1MiAxNy42ODI0IDIwLjk1MzcgMTcuNDgxOSAyMS4wOTM5IDE3LjIzOTlDMjEuMjM0IDE2Ljk5OCAyMS4zMDc3IDE2LjcyMzIgMjEuMzA3MyAxNi40NDM2VjcuNTM5NjFDMjEuMzA3NyA3LjI1OTk5IDIxLjIzNCA2Ljk4NTI1IDIxLjA5MzkgNi43NDMyOUMyMC45NTM3IDYuNTAxMzQgMjAuNzUyIDYuMzAwNzkgMjAuNTA5MyA2LjE2MjAxTDEyLjc4OTcgMS43MTAwMUMxMi41NDYxIDEuNTY3MjEgMTIuMjY4OSAxLjUwMDAxIDExLjk5MTcgMS41MDAwMUgxMi4wMDAxWiIgZmlsbD0iIzlEQ0NFRCIvPgo8cGF0aCBkPSJNNi44OTI1OCAxOS44MDM1TDcuNjA2NTggMTcuODU0N0w5LjAzNDU4IDE5LjAzOTFMNy42OTg5OCAyMC4yNjU1TDYuODkyNTggMTkuODAzNVoiIGZpbGw9IiMyMTMxNDciLz4KPHBhdGggZD0iTTExLjM0NDYgNi45MDk1Mkg5LjM4NzM2QzkuMzE2MzQgNi45MTAyMiA5LjI0NzE1IDYuOTMyMiA5LjE4ODc1IDYuOTcyNjRDOS4xMzAzNSA3LjAxMzA3IDkuMDg1NDIgNy4wNzAwOSA5LjA1OTc2IDcuMTM2MzJMNC44NjgxNiAxOC42Mjc1TDYuODkyNTYgMTkuNzk1MUwxMS41MTI2IDcuMTM2MzJDMTEuNTU0NiA3LjAxODcyIDExLjQ3MDYgNi45MDExMiAxMS4zNTMgNi45MDExMkwxMS4zNDQ2IDYuOTA5NTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTQuNzcyIDYuOTA5NTJIMTIuODE0OEMxMi43NDM4IDYuOTEwMjIgMTIuNjc0NiA2LjkzMjIgMTIuNjE2MiA2Ljk3MjY0QzEyLjU1NzggNy4wMTMwNyAxMi41MTI5IDcuMDcwMDkgMTIuNDg3MiA3LjEzNjMyTDcuNjk5MjIgMjAuMjU3MUw5LjcyMzYyIDIxLjQyNDdMMTQuOTMxNiA3LjEzNjMyQzE0Ljk3MzYgNy4wMTg3MiAxNC44ODk2IDYuOTAxMTIgMTQuNzcyIDYuOTAxMTJWNi45MDk1MloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfOTExNV80MTQyOCI+CjxyZWN0IHdpZHRoPSIyMSIgaGVpZ2h0PSIyMSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNSAxLjUpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==`; 21 | const BASE_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iMyIgZmlsbD0iIzAwNTJGRiIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfOTExNV80MTQyMikiPgo8cGF0aCBkPSJNMTEuOTg2IDIwQzEzLjUzOSAxOS45OTcyIDE1LjA1NzYgMTkuNTQyNSAxNi4zNTY2IDE4LjY5MTNDMTcuNjU1NSAxNy44NDAxIDE4LjY3ODYgMTYuNjI5MiAxOS4zMDEgMTUuMjA2NUMxOS45MjM1IDEzLjc4MzcgMjAuMTE4MyAxMi4yMTA1IDE5Ljg2MTkgMTAuNjc4OEMxOS42MDU0IDkuMTQ3MTUgMTguOTA4NiA3LjcyMzI1IDE3Ljg1NjcgNi41ODA4NUMxNi44MDQ3IDUuNDM4NDQgMTUuNDQyOSA0LjYyNjkyIDEzLjkzNzYgNC4yNDUzQzEyLjQzMjIgMy44NjM2OCAxMC44NDgzIDMuOTI4NDYgOS4zNzkxNCA0LjQzMTc0QzcuOTA5OTYgNC45MzUwMSA2LjYxOTAzIDUuODU1MDIgNS42NjM4NSA3LjA3OTUyQzQuNzA4NjcgOC4zMDQwMSA0LjEzMDU0IDkuNzgwMDUgNCAxMS4zMjc1SDE0LjU5MjZWMTIuNjcyNUg0QzQuMTcwODIgMTQuNjcyNCA1LjA4NjM0IDE2LjUzNTMgNi41NjUzMSAxNy44OTIzQzguMDQ0MjcgMTkuMjQ5MyA5Ljk3ODg1IDIwLjAwMTUgMTEuOTg2IDIwWiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF85MTE1XzQxNDIyIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNCA0KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=`; 22 | const BINANCE_ICON = `${DATA_IMG}PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAwLjAxIDI1MDAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZjNiYTJmO308L3N0eWxlPjwvZGVmcz48dGl0bGU+Ymk8L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNzY0LjQ4LDEwNTAuNTIsMTI1MCw1NjVsNDg1Ljc1LDQ4NS43MywyODIuNS0yODIuNUwxMjUwLDAsNDgyLDc2OGwyODIuNDksMjgyLjVNMCwxMjUwLDI4Mi41MSw5NjcuNDUsNTY1LDEyNDkuOTQsMjgyLjQ5LDE1MzIuNDVabTc2NC40OCwxOTkuNTFMMTI1MCwxOTM1bDQ4NS43NC00ODUuNzIsMjgyLjY1LDI4Mi4zNS0uMTQuMTVMMTI1MCwyNTAwLDQ4MiwxNzMybC0uNC0uNCwyODIuOTEtMjgyLjEyTTE5MzUsMTI1MC4xMmwyODIuNTEtMjgyLjUxTDI1MDAsMTI1MC4xLDIyMTcuNSwxNTMyLjYxWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTE1MzYuNTIsMTI0OS44NWguMTJMMTI1MCw5NjMuMTksMTAzOC4xMywxMTc1aDBsLTI0LjM0LDI0LjM1LTUwLjIsNTAuMjEtLjQuMzkuNC40MUwxMjUwLDE1MzYuODFsMjg2LjY2LTI4Ni42Ni4xNC0uMTYtLjI2LS4xNCIvPjwvZz48L2c+PC9zdmc+`; 23 | const POLYGON_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMyIgZmlsbD0iIzZDMDBGNiIvPgo8cGF0aCBkPSJNMTAuMTY3NCA0TDcuMzYxNzUgNS42MTQzOFYxMC42NTI5TDUuODEzNzEgMTEuNTUxOUw0LjI1NjE4IDEwLjY1MjFWOC44NTMyNkw1LjgxMzcxIDcuOTYyMjVMNi44MTUxNiA4LjU0NDFWNy4wODg3TDUuODA0OTQgNi41MTQxMkwzIDguMTQ2NzRWMTEuMzc2Mkw1LjgxNDQzIDEzTDguNjE5MzUgMTEuMzc2MlY2LjMzODQyTDEwLjE3NjkgNS40Mzg2MkwxMS43MzM2IDYuMzM4NDJWOC4xMjkyM0wxMC4xNzY5IDkuMDM3MDNMOS4xNjY2NiA4LjQ1MDAzVjkuODk4MTZMMTAuMTY3NCAxMC40NzY0TDEzIDguODYyMDVWNS42MTQzOEwxMC4xNjc0IDRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K`; 24 | const OPTIMISM_ICON = `${DATA_IMG}PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iMyIgZmlsbD0iI0VBMzQzMSIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfOTExNV80MTQwMykiPgo8cGF0aCBkPSJNNi41NTM3NCAxNi40OTk1QzUuNTA5MzUgMTYuNDk5NSA0LjY1NDIxIDE2LjI2NjMgMy45ODgzMiAxNS44QzMuMzI5NDQgMTUuMzI3IDMgMTQuNjQ3NSAzIDEzLjc3NDhDMy4wMDM3MSAxMy41NDk0IDMuMDI0NzkgMTMuMzI0NSAzLjA2MzA4IDEzLjEwMkMzLjE3NTIzIDEyLjUwMjUgMy4zMzY0NSAxMS43ODMgMy41NDY3MyAxMC45MzdDNC4xNDI1MiA4LjY0NTMyIDUuNjgyMjQgNy40OTk1IDguMTY1ODkgNy40OTk1QzguNzg4MTkgNy40ODg2OSA5LjQwNjE3IDcuNTk5ODEgOS45ODEzMSA3LjgyNTkyQzEwLjQ5MjIgOC4wMTcwNiAxMC45MzE1IDguMzQ4NzYgMTEuMjQzIDguNzc4NTVDMTEuNTUxNCA5LjE5ODI0IDExLjcwNTYgOS42OTc4NyAxMS43MDU2IDEwLjI3NzRDMTEuNzAxMiAxMC40OTg0IDExLjY4MDIgMTAuNzE4OCAxMS42NDI1IDEwLjkzN0MxMS41MDkzIDExLjY3NjQgMTEuMzU1MSAxMi40MDI1IDExLjE2NTkgMTMuMTAyQzEwLjg1NzUgMTQuMjQxMiAxMC4zMzE4IDE1LjEwMDUgOS41NzQ3NyAxNS42NjY4QzguODI0NzcgMTYuMjI2NCA3LjgxNTQyIDE2LjQ5OTUgNi41NTM3NCAxNi40OTk1Wk02Ljc0Mjk5IDE0LjcwMDhDNy4xOTc3OCAxNC43MTEzIDcuNjQxMTIgMTQuNTY0NSA3Ljk5MDY1IDE0LjI4NzhDOC4zNDExMiAxNC4wMTQ3IDguNTkzNDYgMTMuNTk1IDguNzQwNjUgMTMuMDIyMUM4Ljk0MzkzIDEyLjIzNiA5LjA5ODEzIDExLjU1NjUgOS4yMDMyNyAxMC45NzAzQzkuMjM5OTYgMTAuNzkyNSA5LjI1ODc0IDEwLjYxMTggOS4yNTkzNSAxMC40MzA3QzkuMjU5MzUgOS42NzEyMiA4Ljg0NTc5IDkuMjkxNSA4LjAxMTY4IDkuMjkxNUM3LjU1MjgxIDkuMjgyMTQgNy4xMDU0NiA5LjQyODU4IDYuNzUgOS43MDQ1M0M2LjQwNjU0IDkuOTc3NjYgNi4xNjEyMSAxMC4zOTc0IDYuMDE0MDIgMTAuOTcwM0M1Ljg1MjggMTEuNTI5OCA1LjY5ODYgMTIuMjA5MyA1LjUzNzM4IDEzLjAyMjFDNS41MDAzMyAxMy4xOTUzIDUuNDgxNTQgMTMuMzcxNiA1LjQ4MTMxIDEzLjU0ODRDNS40NzQzIDE0LjMyMTEgNS45MDE4NyAxNC43MDA4IDYuNzQyOTkgMTQuNzAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMi4zMTUzIDE2LjM4QzEyLjI3MzQgMTYuMzgyNyAxMi4yMzE1IDE2LjM3NjMgMTIuMTkyNiAxNi4zNjEzQzEyLjE1MzggMTYuMzQ2MyAxMi4xMTkgMTYuMzIzMSAxMi4wOTEgMTYuMjkzNEMxMi4wNDg0IDE2LjIyNTMgMTIuMDMzNCAxNi4xNDQ3IDEyLjA0ODkgMTYuMDY2OUwxMy44NjQzIDcuOTM5NThDMTMuODcxMSA3Ljg5NDIyIDEzLjg4NzcgNy44NTA2OSAxMy45MTMgNy44MTE3MUMxMy45Mzg0IDcuNzcyNzMgMTMuOTcxOSA3LjczOTE1IDE0LjAxMTUgNy43MTMwOEMxNC4wODIxIDcuNjU2NDggMTQuMTcxNiA3LjYyNTc1IDE0LjI2MzkgNy42MjY0OEgxNy43NjE1QzE4LjczNTggNy42MjY0OCAxOS41MTM5IDcuODE5NjcgMjAuMTAyNyA4LjE5OTM5QzIwLjY5ODQgOC41ODU3NyAyMC45OTk5IDkuMTM4NjkgMjAuOTk5OSA5Ljg2NDgyQzIwLjk5NzMgMTAuMDg0MyAyMC45NzE1IDEwLjMwMzEgMjAuOTIyNyAxMC41MTc3QzIwLjcwNTUgMTEuNDc3IDIwLjI2MzkgMTIuMTgzMSAxOS41OTEgMTIuNjQyOEMxOC45MzIxIDEzLjEwMjQgMTguMDI3OSAxMy4zMjg5IDE2Ljg3ODQgMTMuMzI4OUgxNS4xMDVMMTQuNTAyMiAxNi4wNjY5QzE0LjQ4NTYgMTYuMTU3NiAxNC40MzI5IDE2LjIzODcgMTQuMzU1IDE2LjI5MzRDMTQuMjg0NSAxNi4zNSAxNC4xOTUgMTYuMzgwNyAxNC4xMDI3IDE2LjM4SDEyLjMxNTNaTTE2Ljk2OTUgMTEuNjAzNUMxNy4zMTQyIDExLjYxMiAxNy42NTIgMTEuNTExMiAxNy45Mjk4IDExLjMxNzFDMTguMjE1MiAxMS4xMTYgMTguNDEyNCAxMC44MjE4IDE4LjQ4MzUgMTAuNDkxQzE4LjUwOTUgMTAuMzY4MiAxOC41MjM2IDEwLjI0MzMgMTguNTI1NiAxMC4xMThDMTguNTI1NiA5Ljg3ODE0IDE4LjQ0ODUgOS42OTE2MSAxOC4zMDEzIDkuNTY1MDRDMTguMTU0MSA5LjQzMTgxIDE3Ljg5NDcgOS4zNjUxOSAxNy41MzcyIDkuMzY1MTlIMTUuOTYwMUwxNS40NjI1IDExLjYwMzVIMTYuOTY5NVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfOTExNV80MTQwMyI+CjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSI5IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMyA3LjUpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==`; 25 | 26 | export const CHAIN_INFO: Record = { 27 | // Ethereum Mainnet 28 | 1: { 29 | currencyIcon: ETHER, 30 | icon: ETHEREUM_ICON, 31 | wrappedToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 32 | }, 33 | // Optimism 34 | 10: { 35 | currencyIcon: ETHER, 36 | icon: OPTIMISM_ICON, 37 | wrappedToken: "0x4200000000000000000000000000000000000006", 38 | }, 39 | // Binance Smart Chain 40 | 56: { 41 | icon: BINANCE_ICON, 42 | wrappedToken: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", 43 | }, 44 | // Binance Testnet 45 | 97: { 46 | icon: BINANCE_ICON, 47 | wrappedToken: "0x094616f0bdfb0b526bd735bf66eca0ad254ca81f", 48 | }, 49 | // Gnosis Chain 50 | 100: { 51 | currencyIcon: XDAI, 52 | icon: GNOSIS_ICON, 53 | wrappedToken: "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", 54 | }, 55 | // Polygon 56 | 137: { 57 | icon: POLYGON_ICON, 58 | currencyIcon: POL, 59 | wrappedToken: "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", 60 | }, 61 | // Base 62 | 8453: { 63 | currencyIcon: ETHER, 64 | icon: BASE_ICON, 65 | wrappedToken: "0x4200000000000000000000000000000000000006", 66 | }, 67 | // Gnosis Chiado Testnet 68 | 10200: { 69 | currencyIcon: XDAI, 70 | icon: GNOSIS_ICON, 71 | wrappedToken: "0xb2D0d7aD1D4b2915390Dc7053b9421F735A723E7", 72 | }, 73 | // Arbitrum 74 | 42161: { 75 | currencyIcon: ETHER, 76 | icon: ARBITRUM_ICON, 77 | wrappedToken: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", 78 | }, 79 | // Avalanche 80 | 43114: { 81 | currencyIcon: AVAX_ICON, 82 | icon: AVAX_ICON, 83 | wrappedToken: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", 84 | }, 85 | // Base Sepolia 86 | 84532: { 87 | currencyIcon: ETHER, 88 | icon: BASE_ICON, 89 | wrappedToken: "0x4200000000000000000000000000000000000006", 90 | }, 91 | // Polygon Amoy 92 | 80002: { 93 | currencyIcon: POL, 94 | icon: POLYGON_ICON, 95 | wrappedToken: "0xa5733b3a8e62a8faf43b0376d5faf46e89b3033e", 96 | }, 97 | // Arbitrum Sepolia 98 | 421614: { 99 | currencyIcon: ETHER, 100 | icon: ARBITRUM_ICON, 101 | wrappedToken: "0x980b62da83eff3d4576c647993b0c1d7faf17c73", 102 | }, 103 | // Sepolia 104 | 11155111: { 105 | currencyIcon: ETHER, 106 | icon: ETHEREUM_ICON, 107 | wrappedToken: "0xfff9976782d46cc05630d1f6ebab18b2324d6b14", 108 | }, 109 | 110 | // OP Testnet 111 | 11155420: { 112 | currencyIcon: ETHER, 113 | icon: OPTIMISM_ICON, 114 | wrappedToken: "0x4200000000000000000000000000000000000006", 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /examples/abis/ERC721.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "account", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "sender", 15 | "type": "address" 16 | } 17 | ], 18 | "name": "AdminApproved", 19 | "type": "event" 20 | }, 21 | { 22 | "anonymous": false, 23 | "inputs": [ 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "account", 28 | "type": "address" 29 | }, 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "sender", 34 | "type": "address" 35 | } 36 | ], 37 | "name": "AdminRevoked", 38 | "type": "event" 39 | }, 40 | { 41 | "anonymous": false, 42 | "inputs": [ 43 | { 44 | "indexed": true, 45 | "internalType": "address", 46 | "name": "owner", 47 | "type": "address" 48 | }, 49 | { 50 | "indexed": true, 51 | "internalType": "address", 52 | "name": "approved", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": true, 57 | "internalType": "uint256", 58 | "name": "tokenId", 59 | "type": "uint256" 60 | } 61 | ], 62 | "name": "Approval", 63 | "type": "event" 64 | }, 65 | { 66 | "anonymous": false, 67 | "inputs": [ 68 | { 69 | "indexed": true, 70 | "internalType": "address", 71 | "name": "owner", 72 | "type": "address" 73 | }, 74 | { 75 | "indexed": true, 76 | "internalType": "address", 77 | "name": "operator", 78 | "type": "address" 79 | }, 80 | { 81 | "indexed": false, 82 | "internalType": "bool", 83 | "name": "approved", 84 | "type": "bool" 85 | } 86 | ], 87 | "name": "ApprovalForAll", 88 | "type": "event" 89 | }, 90 | { 91 | "anonymous": false, 92 | "inputs": [ 93 | { 94 | "indexed": false, 95 | "internalType": "address", 96 | "name": "extension", 97 | "type": "address" 98 | } 99 | ], 100 | "name": "ApproveTransferUpdated", 101 | "type": "event" 102 | }, 103 | { 104 | "anonymous": false, 105 | "inputs": [ 106 | { 107 | "indexed": false, 108 | "internalType": "address payable[]", 109 | "name": "receivers", 110 | "type": "address[]" 111 | }, 112 | { 113 | "indexed": false, 114 | "internalType": "uint256[]", 115 | "name": "basisPoints", 116 | "type": "uint256[]" 117 | } 118 | ], 119 | "name": "DefaultRoyaltiesUpdated", 120 | "type": "event" 121 | }, 122 | { 123 | "anonymous": false, 124 | "inputs": [ 125 | { 126 | "indexed": true, 127 | "internalType": "address", 128 | "name": "extension", 129 | "type": "address" 130 | }, 131 | { 132 | "indexed": false, 133 | "internalType": "bool", 134 | "name": "enabled", 135 | "type": "bool" 136 | } 137 | ], 138 | "name": "ExtensionApproveTransferUpdated", 139 | "type": "event" 140 | }, 141 | { 142 | "anonymous": false, 143 | "inputs": [ 144 | { 145 | "indexed": true, 146 | "internalType": "address", 147 | "name": "extension", 148 | "type": "address" 149 | }, 150 | { 151 | "indexed": true, 152 | "internalType": "address", 153 | "name": "sender", 154 | "type": "address" 155 | } 156 | ], 157 | "name": "ExtensionBlacklisted", 158 | "type": "event" 159 | }, 160 | { 161 | "anonymous": false, 162 | "inputs": [ 163 | { 164 | "indexed": true, 165 | "internalType": "address", 166 | "name": "extension", 167 | "type": "address" 168 | }, 169 | { 170 | "indexed": true, 171 | "internalType": "address", 172 | "name": "sender", 173 | "type": "address" 174 | } 175 | ], 176 | "name": "ExtensionRegistered", 177 | "type": "event" 178 | }, 179 | { 180 | "anonymous": false, 181 | "inputs": [ 182 | { 183 | "indexed": true, 184 | "internalType": "address", 185 | "name": "extension", 186 | "type": "address" 187 | }, 188 | { 189 | "indexed": false, 190 | "internalType": "address payable[]", 191 | "name": "receivers", 192 | "type": "address[]" 193 | }, 194 | { 195 | "indexed": false, 196 | "internalType": "uint256[]", 197 | "name": "basisPoints", 198 | "type": "uint256[]" 199 | } 200 | ], 201 | "name": "ExtensionRoyaltiesUpdated", 202 | "type": "event" 203 | }, 204 | { 205 | "anonymous": false, 206 | "inputs": [ 207 | { 208 | "indexed": true, 209 | "internalType": "address", 210 | "name": "extension", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": true, 215 | "internalType": "address", 216 | "name": "sender", 217 | "type": "address" 218 | } 219 | ], 220 | "name": "ExtensionUnregistered", 221 | "type": "event" 222 | }, 223 | { 224 | "anonymous": false, 225 | "inputs": [ 226 | { 227 | "indexed": false, 228 | "internalType": "uint8", 229 | "name": "version", 230 | "type": "uint8" 231 | } 232 | ], 233 | "name": "Initialized", 234 | "type": "event" 235 | }, 236 | { 237 | "anonymous": false, 238 | "inputs": [ 239 | { 240 | "indexed": true, 241 | "internalType": "address", 242 | "name": "extension", 243 | "type": "address" 244 | }, 245 | { 246 | "indexed": true, 247 | "internalType": "address", 248 | "name": "permissions", 249 | "type": "address" 250 | }, 251 | { 252 | "indexed": true, 253 | "internalType": "address", 254 | "name": "sender", 255 | "type": "address" 256 | } 257 | ], 258 | "name": "MintPermissionsUpdated", 259 | "type": "event" 260 | }, 261 | { 262 | "anonymous": false, 263 | "inputs": [ 264 | { 265 | "indexed": true, 266 | "internalType": "address", 267 | "name": "previousOwner", 268 | "type": "address" 269 | }, 270 | { 271 | "indexed": true, 272 | "internalType": "address", 273 | "name": "newOwner", 274 | "type": "address" 275 | } 276 | ], 277 | "name": "OwnershipTransferred", 278 | "type": "event" 279 | }, 280 | { 281 | "anonymous": false, 282 | "inputs": [ 283 | { 284 | "indexed": true, 285 | "internalType": "uint256", 286 | "name": "tokenId", 287 | "type": "uint256" 288 | }, 289 | { 290 | "indexed": false, 291 | "internalType": "address payable[]", 292 | "name": "receivers", 293 | "type": "address[]" 294 | }, 295 | { 296 | "indexed": false, 297 | "internalType": "uint256[]", 298 | "name": "basisPoints", 299 | "type": "uint256[]" 300 | } 301 | ], 302 | "name": "RoyaltiesUpdated", 303 | "type": "event" 304 | }, 305 | { 306 | "anonymous": false, 307 | "inputs": [ 308 | { 309 | "indexed": true, 310 | "internalType": "address", 311 | "name": "from", 312 | "type": "address" 313 | }, 314 | { 315 | "indexed": true, 316 | "internalType": "address", 317 | "name": "to", 318 | "type": "address" 319 | }, 320 | { 321 | "indexed": true, 322 | "internalType": "uint256", 323 | "name": "tokenId", 324 | "type": "uint256" 325 | } 326 | ], 327 | "name": "Transfer", 328 | "type": "event" 329 | }, 330 | { 331 | "inputs": [], 332 | "name": "VERSION", 333 | "outputs": [ 334 | { 335 | "internalType": "uint256", 336 | "name": "", 337 | "type": "uint256" 338 | } 339 | ], 340 | "stateMutability": "view", 341 | "type": "function" 342 | }, 343 | { 344 | "inputs": [ 345 | { 346 | "internalType": "address", 347 | "name": "to", 348 | "type": "address" 349 | }, 350 | { 351 | "internalType": "uint256", 352 | "name": "tokenId", 353 | "type": "uint256" 354 | } 355 | ], 356 | "name": "approve", 357 | "outputs": [], 358 | "stateMutability": "nonpayable", 359 | "type": "function" 360 | }, 361 | { 362 | "inputs": [ 363 | { 364 | "internalType": "address", 365 | "name": "admin", 366 | "type": "address" 367 | } 368 | ], 369 | "name": "approveAdmin", 370 | "outputs": [], 371 | "stateMutability": "nonpayable", 372 | "type": "function" 373 | }, 374 | { 375 | "inputs": [ 376 | { 377 | "internalType": "address", 378 | "name": "owner", 379 | "type": "address" 380 | } 381 | ], 382 | "name": "balanceOf", 383 | "outputs": [ 384 | { 385 | "internalType": "uint256", 386 | "name": "", 387 | "type": "uint256" 388 | } 389 | ], 390 | "stateMutability": "view", 391 | "type": "function" 392 | }, 393 | { 394 | "inputs": [ 395 | { 396 | "internalType": "address", 397 | "name": "extension", 398 | "type": "address" 399 | } 400 | ], 401 | "name": "blacklistExtension", 402 | "outputs": [], 403 | "stateMutability": "nonpayable", 404 | "type": "function" 405 | }, 406 | { 407 | "inputs": [ 408 | { 409 | "internalType": "uint256", 410 | "name": "tokenId", 411 | "type": "uint256" 412 | } 413 | ], 414 | "name": "burn", 415 | "outputs": [], 416 | "stateMutability": "nonpayable", 417 | "type": "function" 418 | }, 419 | { 420 | "inputs": [], 421 | "name": "getAdmins", 422 | "outputs": [ 423 | { 424 | "internalType": "address[]", 425 | "name": "admins", 426 | "type": "address[]" 427 | } 428 | ], 429 | "stateMutability": "view", 430 | "type": "function" 431 | }, 432 | { 433 | "inputs": [], 434 | "name": "getApproveTransfer", 435 | "outputs": [ 436 | { 437 | "internalType": "address", 438 | "name": "", 439 | "type": "address" 440 | } 441 | ], 442 | "stateMutability": "view", 443 | "type": "function" 444 | }, 445 | { 446 | "inputs": [ 447 | { 448 | "internalType": "uint256", 449 | "name": "tokenId", 450 | "type": "uint256" 451 | } 452 | ], 453 | "name": "getApproved", 454 | "outputs": [ 455 | { 456 | "internalType": "address", 457 | "name": "", 458 | "type": "address" 459 | } 460 | ], 461 | "stateMutability": "view", 462 | "type": "function" 463 | }, 464 | { 465 | "inputs": [], 466 | "name": "getExtensions", 467 | "outputs": [ 468 | { 469 | "internalType": "address[]", 470 | "name": "extensions", 471 | "type": "address[]" 472 | } 473 | ], 474 | "stateMutability": "view", 475 | "type": "function" 476 | }, 477 | { 478 | "inputs": [ 479 | { 480 | "internalType": "uint256", 481 | "name": "tokenId", 482 | "type": "uint256" 483 | } 484 | ], 485 | "name": "getFeeBps", 486 | "outputs": [ 487 | { 488 | "internalType": "uint256[]", 489 | "name": "", 490 | "type": "uint256[]" 491 | } 492 | ], 493 | "stateMutability": "view", 494 | "type": "function" 495 | }, 496 | { 497 | "inputs": [ 498 | { 499 | "internalType": "uint256", 500 | "name": "tokenId", 501 | "type": "uint256" 502 | } 503 | ], 504 | "name": "getFeeRecipients", 505 | "outputs": [ 506 | { 507 | "internalType": "address payable[]", 508 | "name": "", 509 | "type": "address[]" 510 | } 511 | ], 512 | "stateMutability": "view", 513 | "type": "function" 514 | }, 515 | { 516 | "inputs": [ 517 | { 518 | "internalType": "uint256", 519 | "name": "tokenId", 520 | "type": "uint256" 521 | } 522 | ], 523 | "name": "getFees", 524 | "outputs": [ 525 | { 526 | "internalType": "address payable[]", 527 | "name": "", 528 | "type": "address[]" 529 | }, 530 | { 531 | "internalType": "uint256[]", 532 | "name": "", 533 | "type": "uint256[]" 534 | } 535 | ], 536 | "stateMutability": "view", 537 | "type": "function" 538 | }, 539 | { 540 | "inputs": [ 541 | { 542 | "internalType": "uint256", 543 | "name": "tokenId", 544 | "type": "uint256" 545 | } 546 | ], 547 | "name": "getRoyalties", 548 | "outputs": [ 549 | { 550 | "internalType": "address payable[]", 551 | "name": "", 552 | "type": "address[]" 553 | }, 554 | { 555 | "internalType": "uint256[]", 556 | "name": "", 557 | "type": "uint256[]" 558 | } 559 | ], 560 | "stateMutability": "view", 561 | "type": "function" 562 | }, 563 | { 564 | "inputs": [ 565 | { 566 | "internalType": "string", 567 | "name": "_name", 568 | "type": "string" 569 | }, 570 | { 571 | "internalType": "string", 572 | "name": "_symbol", 573 | "type": "string" 574 | } 575 | ], 576 | "name": "initialize", 577 | "outputs": [], 578 | "stateMutability": "nonpayable", 579 | "type": "function" 580 | }, 581 | { 582 | "inputs": [ 583 | { 584 | "internalType": "address", 585 | "name": "admin", 586 | "type": "address" 587 | } 588 | ], 589 | "name": "isAdmin", 590 | "outputs": [ 591 | { 592 | "internalType": "bool", 593 | "name": "", 594 | "type": "bool" 595 | } 596 | ], 597 | "stateMutability": "view", 598 | "type": "function" 599 | }, 600 | { 601 | "inputs": [ 602 | { 603 | "internalType": "address", 604 | "name": "owner", 605 | "type": "address" 606 | }, 607 | { 608 | "internalType": "address", 609 | "name": "operator", 610 | "type": "address" 611 | } 612 | ], 613 | "name": "isApprovedForAll", 614 | "outputs": [ 615 | { 616 | "internalType": "bool", 617 | "name": "", 618 | "type": "bool" 619 | } 620 | ], 621 | "stateMutability": "view", 622 | "type": "function" 623 | }, 624 | { 625 | "inputs": [ 626 | { 627 | "internalType": "address", 628 | "name": "to", 629 | "type": "address" 630 | } 631 | ], 632 | "name": "mintBase", 633 | "outputs": [ 634 | { 635 | "internalType": "uint256", 636 | "name": "", 637 | "type": "uint256" 638 | } 639 | ], 640 | "stateMutability": "nonpayable", 641 | "type": "function" 642 | }, 643 | { 644 | "inputs": [ 645 | { 646 | "internalType": "address", 647 | "name": "to", 648 | "type": "address" 649 | }, 650 | { 651 | "internalType": "string", 652 | "name": "uri", 653 | "type": "string" 654 | } 655 | ], 656 | "name": "mintBase", 657 | "outputs": [ 658 | { 659 | "internalType": "uint256", 660 | "name": "", 661 | "type": "uint256" 662 | } 663 | ], 664 | "stateMutability": "nonpayable", 665 | "type": "function" 666 | }, 667 | { 668 | "inputs": [ 669 | { 670 | "internalType": "address", 671 | "name": "to", 672 | "type": "address" 673 | }, 674 | { 675 | "internalType": "string[]", 676 | "name": "uris", 677 | "type": "string[]" 678 | } 679 | ], 680 | "name": "mintBaseBatch", 681 | "outputs": [ 682 | { 683 | "internalType": "uint256[]", 684 | "name": "tokenIds", 685 | "type": "uint256[]" 686 | } 687 | ], 688 | "stateMutability": "nonpayable", 689 | "type": "function" 690 | }, 691 | { 692 | "inputs": [ 693 | { 694 | "internalType": "address", 695 | "name": "to", 696 | "type": "address" 697 | }, 698 | { 699 | "internalType": "uint16", 700 | "name": "count", 701 | "type": "uint16" 702 | } 703 | ], 704 | "name": "mintBaseBatch", 705 | "outputs": [ 706 | { 707 | "internalType": "uint256[]", 708 | "name": "tokenIds", 709 | "type": "uint256[]" 710 | } 711 | ], 712 | "stateMutability": "nonpayable", 713 | "type": "function" 714 | }, 715 | { 716 | "inputs": [ 717 | { 718 | "internalType": "address", 719 | "name": "to", 720 | "type": "address" 721 | } 722 | ], 723 | "name": "mintExtension", 724 | "outputs": [ 725 | { 726 | "internalType": "uint256", 727 | "name": "", 728 | "type": "uint256" 729 | } 730 | ], 731 | "stateMutability": "nonpayable", 732 | "type": "function" 733 | }, 734 | { 735 | "inputs": [ 736 | { 737 | "internalType": "address", 738 | "name": "to", 739 | "type": "address" 740 | }, 741 | { 742 | "internalType": "uint80", 743 | "name": "data", 744 | "type": "uint80" 745 | } 746 | ], 747 | "name": "mintExtension", 748 | "outputs": [ 749 | { 750 | "internalType": "uint256", 751 | "name": "", 752 | "type": "uint256" 753 | } 754 | ], 755 | "stateMutability": "nonpayable", 756 | "type": "function" 757 | }, 758 | { 759 | "inputs": [ 760 | { 761 | "internalType": "address", 762 | "name": "to", 763 | "type": "address" 764 | }, 765 | { 766 | "internalType": "string", 767 | "name": "uri", 768 | "type": "string" 769 | } 770 | ], 771 | "name": "mintExtension", 772 | "outputs": [ 773 | { 774 | "internalType": "uint256", 775 | "name": "", 776 | "type": "uint256" 777 | } 778 | ], 779 | "stateMutability": "nonpayable", 780 | "type": "function" 781 | }, 782 | { 783 | "inputs": [ 784 | { 785 | "internalType": "address", 786 | "name": "to", 787 | "type": "address" 788 | }, 789 | { 790 | "internalType": "string[]", 791 | "name": "uris", 792 | "type": "string[]" 793 | } 794 | ], 795 | "name": "mintExtensionBatch", 796 | "outputs": [ 797 | { 798 | "internalType": "uint256[]", 799 | "name": "tokenIds", 800 | "type": "uint256[]" 801 | } 802 | ], 803 | "stateMutability": "nonpayable", 804 | "type": "function" 805 | }, 806 | { 807 | "inputs": [ 808 | { 809 | "internalType": "address", 810 | "name": "to", 811 | "type": "address" 812 | }, 813 | { 814 | "internalType": "uint80[]", 815 | "name": "data", 816 | "type": "uint80[]" 817 | } 818 | ], 819 | "name": "mintExtensionBatch", 820 | "outputs": [ 821 | { 822 | "internalType": "uint256[]", 823 | "name": "tokenIds", 824 | "type": "uint256[]" 825 | } 826 | ], 827 | "stateMutability": "nonpayable", 828 | "type": "function" 829 | }, 830 | { 831 | "inputs": [ 832 | { 833 | "internalType": "address", 834 | "name": "to", 835 | "type": "address" 836 | }, 837 | { 838 | "internalType": "uint16", 839 | "name": "count", 840 | "type": "uint16" 841 | } 842 | ], 843 | "name": "mintExtensionBatch", 844 | "outputs": [ 845 | { 846 | "internalType": "uint256[]", 847 | "name": "tokenIds", 848 | "type": "uint256[]" 849 | } 850 | ], 851 | "stateMutability": "nonpayable", 852 | "type": "function" 853 | }, 854 | { 855 | "inputs": [], 856 | "name": "name", 857 | "outputs": [ 858 | { 859 | "internalType": "string", 860 | "name": "", 861 | "type": "string" 862 | } 863 | ], 864 | "stateMutability": "view", 865 | "type": "function" 866 | }, 867 | { 868 | "inputs": [], 869 | "name": "owner", 870 | "outputs": [ 871 | { 872 | "internalType": "address", 873 | "name": "", 874 | "type": "address" 875 | } 876 | ], 877 | "stateMutability": "view", 878 | "type": "function" 879 | }, 880 | { 881 | "inputs": [ 882 | { 883 | "internalType": "uint256", 884 | "name": "tokenId", 885 | "type": "uint256" 886 | } 887 | ], 888 | "name": "ownerOf", 889 | "outputs": [ 890 | { 891 | "internalType": "address", 892 | "name": "", 893 | "type": "address" 894 | } 895 | ], 896 | "stateMutability": "view", 897 | "type": "function" 898 | }, 899 | { 900 | "inputs": [ 901 | { 902 | "internalType": "address", 903 | "name": "extension", 904 | "type": "address" 905 | }, 906 | { 907 | "internalType": "string", 908 | "name": "baseURI", 909 | "type": "string" 910 | } 911 | ], 912 | "name": "registerExtension", 913 | "outputs": [], 914 | "stateMutability": "nonpayable", 915 | "type": "function" 916 | }, 917 | { 918 | "inputs": [ 919 | { 920 | "internalType": "address", 921 | "name": "extension", 922 | "type": "address" 923 | }, 924 | { 925 | "internalType": "string", 926 | "name": "baseURI", 927 | "type": "string" 928 | }, 929 | { 930 | "internalType": "bool", 931 | "name": "baseURIIdentical", 932 | "type": "bool" 933 | } 934 | ], 935 | "name": "registerExtension", 936 | "outputs": [], 937 | "stateMutability": "nonpayable", 938 | "type": "function" 939 | }, 940 | { 941 | "inputs": [], 942 | "name": "renounceOwnership", 943 | "outputs": [], 944 | "stateMutability": "nonpayable", 945 | "type": "function" 946 | }, 947 | { 948 | "inputs": [ 949 | { 950 | "internalType": "address", 951 | "name": "admin", 952 | "type": "address" 953 | } 954 | ], 955 | "name": "revokeAdmin", 956 | "outputs": [], 957 | "stateMutability": "nonpayable", 958 | "type": "function" 959 | }, 960 | { 961 | "inputs": [ 962 | { 963 | "internalType": "uint256", 964 | "name": "tokenId", 965 | "type": "uint256" 966 | }, 967 | { 968 | "internalType": "uint256", 969 | "name": "value", 970 | "type": "uint256" 971 | } 972 | ], 973 | "name": "royaltyInfo", 974 | "outputs": [ 975 | { 976 | "internalType": "address", 977 | "name": "", 978 | "type": "address" 979 | }, 980 | { 981 | "internalType": "uint256", 982 | "name": "", 983 | "type": "uint256" 984 | } 985 | ], 986 | "stateMutability": "view", 987 | "type": "function" 988 | }, 989 | { 990 | "inputs": [ 991 | { 992 | "internalType": "address", 993 | "name": "from", 994 | "type": "address" 995 | }, 996 | { 997 | "internalType": "address", 998 | "name": "to", 999 | "type": "address" 1000 | }, 1001 | { 1002 | "internalType": "uint256", 1003 | "name": "tokenId", 1004 | "type": "uint256" 1005 | } 1006 | ], 1007 | "name": "safeTransferFrom", 1008 | "outputs": [], 1009 | "stateMutability": "nonpayable", 1010 | "type": "function" 1011 | }, 1012 | { 1013 | "inputs": [ 1014 | { 1015 | "internalType": "address", 1016 | "name": "from", 1017 | "type": "address" 1018 | }, 1019 | { 1020 | "internalType": "address", 1021 | "name": "to", 1022 | "type": "address" 1023 | }, 1024 | { 1025 | "internalType": "uint256", 1026 | "name": "tokenId", 1027 | "type": "uint256" 1028 | }, 1029 | { 1030 | "internalType": "bytes", 1031 | "name": "data", 1032 | "type": "bytes" 1033 | } 1034 | ], 1035 | "name": "safeTransferFrom", 1036 | "outputs": [], 1037 | "stateMutability": "nonpayable", 1038 | "type": "function" 1039 | }, 1040 | { 1041 | "inputs": [ 1042 | { 1043 | "internalType": "address", 1044 | "name": "operator", 1045 | "type": "address" 1046 | }, 1047 | { 1048 | "internalType": "bool", 1049 | "name": "approved", 1050 | "type": "bool" 1051 | } 1052 | ], 1053 | "name": "setApprovalForAll", 1054 | "outputs": [], 1055 | "stateMutability": "nonpayable", 1056 | "type": "function" 1057 | }, 1058 | { 1059 | "inputs": [ 1060 | { 1061 | "internalType": "address", 1062 | "name": "extension", 1063 | "type": "address" 1064 | } 1065 | ], 1066 | "name": "setApproveTransfer", 1067 | "outputs": [], 1068 | "stateMutability": "nonpayable", 1069 | "type": "function" 1070 | }, 1071 | { 1072 | "inputs": [ 1073 | { 1074 | "internalType": "bool", 1075 | "name": "enabled", 1076 | "type": "bool" 1077 | } 1078 | ], 1079 | "name": "setApproveTransferExtension", 1080 | "outputs": [], 1081 | "stateMutability": "nonpayable", 1082 | "type": "function" 1083 | }, 1084 | { 1085 | "inputs": [ 1086 | { 1087 | "internalType": "string", 1088 | "name": "uri", 1089 | "type": "string" 1090 | } 1091 | ], 1092 | "name": "setBaseTokenURI", 1093 | "outputs": [], 1094 | "stateMutability": "nonpayable", 1095 | "type": "function" 1096 | }, 1097 | { 1098 | "inputs": [ 1099 | { 1100 | "internalType": "string", 1101 | "name": "uri", 1102 | "type": "string" 1103 | } 1104 | ], 1105 | "name": "setBaseTokenURIExtension", 1106 | "outputs": [], 1107 | "stateMutability": "nonpayable", 1108 | "type": "function" 1109 | }, 1110 | { 1111 | "inputs": [ 1112 | { 1113 | "internalType": "string", 1114 | "name": "uri", 1115 | "type": "string" 1116 | }, 1117 | { 1118 | "internalType": "bool", 1119 | "name": "identical", 1120 | "type": "bool" 1121 | } 1122 | ], 1123 | "name": "setBaseTokenURIExtension", 1124 | "outputs": [], 1125 | "stateMutability": "nonpayable", 1126 | "type": "function" 1127 | }, 1128 | { 1129 | "inputs": [ 1130 | { 1131 | "internalType": "address", 1132 | "name": "extension", 1133 | "type": "address" 1134 | }, 1135 | { 1136 | "internalType": "address", 1137 | "name": "permissions", 1138 | "type": "address" 1139 | } 1140 | ], 1141 | "name": "setMintPermissions", 1142 | "outputs": [], 1143 | "stateMutability": "nonpayable", 1144 | "type": "function" 1145 | }, 1146 | { 1147 | "inputs": [ 1148 | { 1149 | "internalType": "uint256", 1150 | "name": "tokenId", 1151 | "type": "uint256" 1152 | }, 1153 | { 1154 | "internalType": "address payable[]", 1155 | "name": "receivers", 1156 | "type": "address[]" 1157 | }, 1158 | { 1159 | "internalType": "uint256[]", 1160 | "name": "basisPoints", 1161 | "type": "uint256[]" 1162 | } 1163 | ], 1164 | "name": "setRoyalties", 1165 | "outputs": [], 1166 | "stateMutability": "nonpayable", 1167 | "type": "function" 1168 | }, 1169 | { 1170 | "inputs": [ 1171 | { 1172 | "internalType": "address payable[]", 1173 | "name": "receivers", 1174 | "type": "address[]" 1175 | }, 1176 | { 1177 | "internalType": "uint256[]", 1178 | "name": "basisPoints", 1179 | "type": "uint256[]" 1180 | } 1181 | ], 1182 | "name": "setRoyalties", 1183 | "outputs": [], 1184 | "stateMutability": "nonpayable", 1185 | "type": "function" 1186 | }, 1187 | { 1188 | "inputs": [ 1189 | { 1190 | "internalType": "address", 1191 | "name": "extension", 1192 | "type": "address" 1193 | }, 1194 | { 1195 | "internalType": "address payable[]", 1196 | "name": "receivers", 1197 | "type": "address[]" 1198 | }, 1199 | { 1200 | "internalType": "uint256[]", 1201 | "name": "basisPoints", 1202 | "type": "uint256[]" 1203 | } 1204 | ], 1205 | "name": "setRoyaltiesExtension", 1206 | "outputs": [], 1207 | "stateMutability": "nonpayable", 1208 | "type": "function" 1209 | }, 1210 | { 1211 | "inputs": [ 1212 | { 1213 | "internalType": "uint256", 1214 | "name": "tokenId", 1215 | "type": "uint256" 1216 | }, 1217 | { 1218 | "internalType": "string", 1219 | "name": "uri", 1220 | "type": "string" 1221 | } 1222 | ], 1223 | "name": "setTokenURI", 1224 | "outputs": [], 1225 | "stateMutability": "nonpayable", 1226 | "type": "function" 1227 | }, 1228 | { 1229 | "inputs": [ 1230 | { 1231 | "internalType": "uint256[]", 1232 | "name": "tokenIds", 1233 | "type": "uint256[]" 1234 | }, 1235 | { 1236 | "internalType": "string[]", 1237 | "name": "uris", 1238 | "type": "string[]" 1239 | } 1240 | ], 1241 | "name": "setTokenURI", 1242 | "outputs": [], 1243 | "stateMutability": "nonpayable", 1244 | "type": "function" 1245 | }, 1246 | { 1247 | "inputs": [ 1248 | { 1249 | "internalType": "uint256[]", 1250 | "name": "tokenIds", 1251 | "type": "uint256[]" 1252 | }, 1253 | { 1254 | "internalType": "string[]", 1255 | "name": "uris", 1256 | "type": "string[]" 1257 | } 1258 | ], 1259 | "name": "setTokenURIExtension", 1260 | "outputs": [], 1261 | "stateMutability": "nonpayable", 1262 | "type": "function" 1263 | }, 1264 | { 1265 | "inputs": [ 1266 | { 1267 | "internalType": "uint256", 1268 | "name": "tokenId", 1269 | "type": "uint256" 1270 | }, 1271 | { 1272 | "internalType": "string", 1273 | "name": "uri", 1274 | "type": "string" 1275 | } 1276 | ], 1277 | "name": "setTokenURIExtension", 1278 | "outputs": [], 1279 | "stateMutability": "nonpayable", 1280 | "type": "function" 1281 | }, 1282 | { 1283 | "inputs": [ 1284 | { 1285 | "internalType": "string", 1286 | "name": "prefix", 1287 | "type": "string" 1288 | } 1289 | ], 1290 | "name": "setTokenURIPrefix", 1291 | "outputs": [], 1292 | "stateMutability": "nonpayable", 1293 | "type": "function" 1294 | }, 1295 | { 1296 | "inputs": [ 1297 | { 1298 | "internalType": "string", 1299 | "name": "prefix", 1300 | "type": "string" 1301 | } 1302 | ], 1303 | "name": "setTokenURIPrefixExtension", 1304 | "outputs": [], 1305 | "stateMutability": "nonpayable", 1306 | "type": "function" 1307 | }, 1308 | { 1309 | "inputs": [ 1310 | { 1311 | "internalType": "bytes4", 1312 | "name": "interfaceId", 1313 | "type": "bytes4" 1314 | } 1315 | ], 1316 | "name": "supportsInterface", 1317 | "outputs": [ 1318 | { 1319 | "internalType": "bool", 1320 | "name": "", 1321 | "type": "bool" 1322 | } 1323 | ], 1324 | "stateMutability": "view", 1325 | "type": "function" 1326 | }, 1327 | { 1328 | "inputs": [], 1329 | "name": "symbol", 1330 | "outputs": [ 1331 | { 1332 | "internalType": "string", 1333 | "name": "", 1334 | "type": "string" 1335 | } 1336 | ], 1337 | "stateMutability": "view", 1338 | "type": "function" 1339 | }, 1340 | { 1341 | "inputs": [ 1342 | { 1343 | "internalType": "uint256", 1344 | "name": "tokenId", 1345 | "type": "uint256" 1346 | } 1347 | ], 1348 | "name": "tokenData", 1349 | "outputs": [ 1350 | { 1351 | "internalType": "uint80", 1352 | "name": "", 1353 | "type": "uint80" 1354 | } 1355 | ], 1356 | "stateMutability": "view", 1357 | "type": "function" 1358 | }, 1359 | { 1360 | "inputs": [ 1361 | { 1362 | "internalType": "uint256", 1363 | "name": "tokenId", 1364 | "type": "uint256" 1365 | } 1366 | ], 1367 | "name": "tokenExtension", 1368 | "outputs": [ 1369 | { 1370 | "internalType": "address", 1371 | "name": "extension", 1372 | "type": "address" 1373 | } 1374 | ], 1375 | "stateMutability": "view", 1376 | "type": "function" 1377 | }, 1378 | { 1379 | "inputs": [ 1380 | { 1381 | "internalType": "uint256", 1382 | "name": "tokenId", 1383 | "type": "uint256" 1384 | } 1385 | ], 1386 | "name": "tokenURI", 1387 | "outputs": [ 1388 | { 1389 | "internalType": "string", 1390 | "name": "", 1391 | "type": "string" 1392 | } 1393 | ], 1394 | "stateMutability": "view", 1395 | "type": "function" 1396 | }, 1397 | { 1398 | "inputs": [ 1399 | { 1400 | "internalType": "address", 1401 | "name": "from", 1402 | "type": "address" 1403 | }, 1404 | { 1405 | "internalType": "address", 1406 | "name": "to", 1407 | "type": "address" 1408 | }, 1409 | { 1410 | "internalType": "uint256", 1411 | "name": "tokenId", 1412 | "type": "uint256" 1413 | } 1414 | ], 1415 | "name": "transferFrom", 1416 | "outputs": [], 1417 | "stateMutability": "nonpayable", 1418 | "type": "function" 1419 | }, 1420 | { 1421 | "inputs": [ 1422 | { 1423 | "internalType": "address", 1424 | "name": "newOwner", 1425 | "type": "address" 1426 | } 1427 | ], 1428 | "name": "transferOwnership", 1429 | "outputs": [], 1430 | "stateMutability": "nonpayable", 1431 | "type": "function" 1432 | }, 1433 | { 1434 | "inputs": [ 1435 | { 1436 | "internalType": "address", 1437 | "name": "extension", 1438 | "type": "address" 1439 | } 1440 | ], 1441 | "name": "unregisterExtension", 1442 | "outputs": [], 1443 | "stateMutability": "nonpayable", 1444 | "type": "function" 1445 | } 1446 | ] --------------------------------------------------------------------------------