From 0c11b97fd103ed5fa2f16deaa785997691fd4829 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 10 Oct 2024 11:58:37 +0200 Subject: [PATCH] Mock Adapter with Deterministic Private Key (#130) --- .github/workflows/e2e.yaml | 1 + src/chains/ethereum.ts | 21 +++++- src/constants.ts | 6 +- src/mpcContract.ts | 14 +++- src/types.ts | 4 +- src/utils/index.ts | 2 + src/utils/mock-sign.ts | 106 +++++++++++++++++++++++++++++ tests/e2e.test.ts | 45 ++++++++---- tests/unit/utils.mock-sign.test.ts | 22 ++++++ 9 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 src/utils/mock-sign.ts create mode 100644 tests/unit/utils.mock-sign.test.ts diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 22cacbb..92270a0 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -21,3 +21,4 @@ jobs: MPC_CONTRACT_ID: v1.signer-prod.testnet NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}} NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}} + ETH_PK: ${{secrets.ETH_PK}} diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index 84de6de..e1d969c 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -16,7 +16,6 @@ import { FunctionCallTransaction, TxPayload, SignArgs, - MpcContract, Network, buildTxPayload, populateTx, @@ -24,18 +23,19 @@ import { broadcastSignedTransaction, SignRequestData, NearEthTxData, + IMpcContract, } from ".."; import { Beta } from "../beta"; import { requestRouter } from "../utils/request"; export class NearEthAdapter { - readonly mpcContract: MpcContract; + readonly mpcContract: IMpcContract; readonly address: Address; readonly derivationPath: string; readonly beta: Beta; private constructor(config: { - mpcContract: MpcContract; + mpcContract: IMpcContract; derivationPath: string; sender: Address; }) { @@ -78,6 +78,21 @@ export class NearEthAdapter { }); } + /** + * Constructs an EVM instance with the provided configuration. + * @param {AdapterParams} args - The configuration object for the Adapter instance. + */ + static async mocked(args: AdapterParams): Promise { + // Sender is uniquely determined by the derivation path! + const mpcContract = args.mpcContract; + const derivationPath = args.derivationPath || "ethereum,1"; + return new NearEthAdapter({ + sender: await mpcContract.deriveEthAddress(derivationPath), + derivationPath, + mpcContract, + }); + } + /** * Takes a minimally declared Ethereum Transaction, * builds the full transaction payload (with gas estimates, prices etc...), diff --git a/src/constants.ts b/src/constants.ts index 1639615..8e77156 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,6 @@ /// A short list of networks with known wrapped tokens. -const ETHER_ICON = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjYgNiA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfOTExNV80MDk4NSkiPg0KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMjQiIGZpbGw9IiM2NzgwRTMiLz4NCjwvZz4NCjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF85MTE1XzQwOTg1KSI+DQo8cGF0aCBkPSJNMzAuNDk4MiAxNUwzMC4zMTI1IDE1LjY2MDlWMzQuODM2M0wzMC40OTgyIDM1LjAzMDRMMzguOTk1NSAyOS43NjlMMzAuNDk4MiAxNVoiIGZpbGw9IiNDMkNCRjMiLz4NCjxwYXRoIGQ9Ik0zMC40OTc1IDE1TDIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDRWMjUuNzIzMlYxNVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNDk5MiAzNi43MTU3TDMwLjM5NDUgMzYuODQ5NFY0My42Nzk5TDMwLjQ5OTIgNDQuMDAwMUwzOS4wMDE3IDMxLjQ1N0wzMC40OTkyIDM2LjcxNTdaIiBmaWxsPSIjQzBDQUYyIi8+DQo8cGF0aCBkPSJNMzAuNDk3NSA0NC4wMDAxVjM2LjcxNTdMMjIgMzEuNDU3TDMwLjQ5NzUgNDQuMDAwMVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNSAzNS4wMzAzTDM4Ljk5NzMgMjkuNzY5TDMwLjUgMjUuNzIzMVYzNS4wMzAzWiIgZmlsbD0iIzg1OThFOCIvPg0KPHBhdGggZD0iTTIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDNWMjUuNzIzMUwyMiAyOS43NjlaIiBmaWxsPSIjQzJDQkYzIi8+DQo8L2c+DQo8ZGVmcz4NCjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZF85MTE1XzQwOTg1IiB4PSIwIiB5PSIwIiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4NCjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+DQo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4NCjxmZU9mZnNldC8+DQo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIzIi8+DQo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4NCjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA3IDAiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiIHJlc3VsdD0ic2hhcGUiLz4NCjwvZmlsdGVyPg0KPGNsaXBQYXRoIGlkPSJjbGlwMF85MTE1XzQwOTg1Ij4NCjxyZWN0IHdpZHRoPSIxNyIgaGVpZ2h0PSIyOSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyIDE1KSIvPg0KPC9jbGlwUGF0aD4NCjwvZGVmcz4NCjwvc3ZnPg0K"; +const ETHER_ICON = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjYgNiA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfOTExNV80MDk4NSkiPg0KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMjQiIGZpbGw9IiM2NzgwRTMiLz4NCjwvZz4NCjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF85MTE1XzQwOTg1KSI+DQo8cGF0aCBkPSJNMzAuNDk4MiAxNUwzMC4zMTI1IDE1LjY2MDlWMzQuODM2M0wzMC40OTgyIDM1LjAzMDRMMzguOTk1NSAyOS43NjlMMzAuNDk4MiAxNVoiIGZpbGw9IiNDMkNCRjMiLz4NCjxwYXRoIGQ9Ik0zMC40OTc1IDE1TDIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDRWMjUuNzIzMlYxNVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNDk5MiAzNi43MTU3TDMwLjM5NDUgMzYuODQ5NFY0My42Nzk5TDMwLjQ5OTIgNDQuMDAwMUwzOS4wMDE3IDMxLjQ1N0wzMC40OTkyIDM2LjcxNTdaIiBmaWxsPSIjQzBDQUYyIi8+DQo8cGF0aCBkPSJNMzAuNDk3NSA0NC4wMDAxVjM2LjcxNTdMMjIgMzEuNDU3TDMwLjQ5NzUgNDQuMDAwMVoiIGZpbGw9IndoaXRlIi8+DQo8cGF0aCBkPSJNMzAuNSAzNS4wMzAzTDM4Ljk5NzMgMjkuNzY5TDMwLjUgMjUuNzIzMVYzNS4wMzAzWiIgZmlsbD0iIzg1OThFOCIvPg0KPHBhdGggZD0iTTIyIDI5Ljc2OUwzMC40OTc1IDM1LjAzMDNWMjUuNzIzMUwyMiAyOS43NjlaIiBmaWxsPSIjQzJDQkYzIi8+DQo8L2c+DQo8ZGVmcz4NCjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZF85MTE1XzQwOTg1IiB4PSIwIiB5PSIwIiB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4NCjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+DQo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4NCjxmZU9mZnNldC8+DQo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIzIi8+DQo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4NCjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA3IDAiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiLz4NCjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzkxMTVfNDA5ODUiIHJlc3VsdD0ic2hhcGUiLz4NCjwvZmlsdGVyPg0KPGNsaXBQYXRoIGlkPSJjbGlwMF85MTE1XzQwOTg1Ij4NCjxyZWN0IHdpZHRoPSIxNyIgaGVpZ2h0PSIyOSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyIDE1KSIvPg0KPC9jbGlwUGF0aD4NCjwvZGVmcz4NCjwvc3ZnPg0K"; interface ChainInfo { currencyIcon?: string; icon?: string; @@ -29,7 +30,8 @@ export const CHAIN_INFO: Record = { wrappedToken: "0x094616f0bdfb0b526bd735bf66eca0ad254ca81f", }, 100: { - currencyIcon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjM3NHB4IiBoZWlnaHQ9IjM3NHB4IiBzdHlsZT0ic2hhcGUtcmVuZGVyaW5nOmdlb21ldHJpY1ByZWNpc2lvbjsgdGV4dC1yZW5kZXJpbmc6Z2VvbWV0cmljUHJlY2lzaW9uOyBpbWFnZS1yZW5kZXJpbmc6b3B0aW1pemVRdWFsaXR5OyBmaWxsLXJ1bGU6ZXZlbm9kZDsgY2xpcC1ydWxlOmV2ZW5vZGQiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGc+PHBhdGggc3R5bGU9Im9wYWNpdHk6MSIgZmlsbD0iIzAwMzM3MCIgZD0iTSA0Mi41LC0wLjUgQyAxMzguNSwtMC41IDIzNC41LC0wLjUgMzMwLjUsLTAuNUMgMzUzLjE2Nyw1LjUgMzY3LjUsMTkuODMzMyAzNzMuNSw0Mi41QyAzNzMuNSwxMzguNSAzNzMuNSwyMzQuNSAzNzMuNSwzMzAuNUMgMzY3LjUsMzUzLjE2NyAzNTMuMTY3LDM2Ny41IDMzMC41LDM3My41QyAyMzQuNSwzNzMuNSAxMzguNSwzNzMuNSA0Mi41LDM3My41QyAxOS44MzMzLDM2Ny41IDUuNSwzNTMuMTY3IC0wLjUsMzMwLjVDIC0wLjUsMjM0LjUgLTAuNSwxMzguNSAtMC41LDQyLjVDIDUuNSwxOS44MzMzIDE5LjgzMzMsNS41IDQyLjUsLTAuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGFlMTNhIiBkPSJNIDE4Ny41LDE0My41IEMgMTk1LjgzMywxNTIuMTY3IDIwNC4xNjcsMTYwLjgzMyAyMTIuNSwxNjkuNUMgMjE5LjcxNCwxODIuMzI2IDIxOC44OCwxOTQuNjU5IDIxMCwyMDYuNUMgMjAyLjI3MiwyMTQuMzk2IDE5NC40MzgsMjIyLjA2MyAxODYuNSwyMjkuNUMgMTY5LjM2OCwyNDcuMTMyIDE1Mi4wMzUsMjY0LjYzMiAxMzQuNSwyODJDIDEyMS45MzIsMjkxLjE4MSAxMDguOTMyLDI5MS44NDggOTUuNSwyODRDIDgyLjU0MzEsMjcyLjQzNCA4MC4wNDMxLDI1OC45MzQgODgsMjQzLjVDIDEwNC4zMDQsMjI2LjE5NSAxMjAuOTcxLDIwOS4xOTUgMTM4LDE5Mi41QyAxNDAuNzg2LDE4OS4wMzMgMTQxLjEyLDE4NS4zNjYgMTM5LDE4MS41QyAxMjIuNjY3LDE2NS4xNjcgMTA2LjMzMywxNDguODMzIDkwLDEzMi41QyA3OS44MjQ5LDExNi40MDYgODEuNjU4MiwxMDEuOTA2IDk1LjUsODlDIDEwOC45Niw4MS4xMTk5IDEyMS45Niw4MS43ODY1IDEzNC41LDkxQyAxNTIuMDM1LDEwOC43MDIgMTY5LjcwMSwxMjYuMjAyIDE4Ny41LDE0My41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwYWUxM2EiIGQ9Ik0gMjc2LjUsMTM5LjUgQyAyNjAuMDA1LDE0Ny4wMzYgMjQ2LjE3MiwxNDMuNzAyIDIzNSwxMjkuNUMgMjI5LjMxMywxMTguODA5IDIyOS4xNDYsMTA4LjE0MyAyMzQuNSw5Ny41QyAyNDYuNjU1LDgyLjEzNyAyNjEuMzIyLDc5LjMwMzcgMjc4LjUsODlDIDI5MS45OTQsMTAxLjMyMiAyOTQuMTYxLDExNS40ODkgMjg1LDEzMS41QyAyODIuMjcxLDEzNC4zOTkgMjc5LjQzOCwxMzcuMDY2IDI3Ni41LDEzOS41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwNDc4NWIiIGQ9Ik0gMjM0LjUsOTcuNSBDIDIyOS4xNDYsMTA4LjE0MyAyMjkuMzEzLDExOC44MDkgMjM1LDEyOS41QyAyNDYuMTcyLDE0My43MDIgMjYwLjAwNSwxNDcuMDM2IDI3Ni41LDEzOS41QyAyNjIuODY5LDE1My42MzIgMjQ5LjAzNSwxNjcuNjMyIDIzNSwxODEuNUMgMjMzLjE4MiwxODQuNDE2IDIzMy4wMTYsMTg3LjQxNiAyMzQuNSwxOTAuNUMgMjI3LjE2NywxODMuNSAyMTkuODMzLDE3Ni41IDIxMi41LDE2OS41QyAyMDQuMTY3LDE2MC44MzMgMTk1LjgzMywxNTIuMTY3IDE4Ny41LDE0My41QyAyMDMuMTQsMTI4LjE5NCAyMTguODA3LDExMi44NjEgMjM0LjUsOTcuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGRiNDBiIiBkPSJNIDIxMi41LDE2OS41IEMgMjE5LjgzMywxNzYuNSAyMjcuMTY3LDE4My41IDIzNC41LDE5MC41QyAyNTEuMTMyLDIwNy42MzIgMjY3Ljk2NSwyMjQuNjMyIDI4NSwyNDEuNUMgMjk0LjA2NywyNTcuNDIgMjkxLjkwMSwyNzEuNTg3IDI3OC41LDI4NEMgMjY3LjA1NSwyOTAuOTM1IDI1NS4zODksMjkxLjI2OSAyNDMuNSwyODVDIDIyNC45MzYsMjY1LjkzNCAyMDUuOTM2LDI0Ny40MzQgMTg2LjUsMjI5LjVDIDE5NC40MzgsMjIyLjA2MyAyMDIuMjcyLDIxNC4zOTYgMjEwLDIwNi41QyAyMTguODgsMTk0LjY1OSAyMTkuNzE0LDE4Mi4zMjYgMjEyLjUsMTY5LjUgWiIvPjwvZz4KPC9zdmc+Cg==", + currencyIcon: + "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjM3NHB4IiBoZWlnaHQ9IjM3NHB4IiBzdHlsZT0ic2hhcGUtcmVuZGVyaW5nOmdlb21ldHJpY1ByZWNpc2lvbjsgdGV4dC1yZW5kZXJpbmc6Z2VvbWV0cmljUHJlY2lzaW9uOyBpbWFnZS1yZW5kZXJpbmc6b3B0aW1pemVRdWFsaXR5OyBmaWxsLXJ1bGU6ZXZlbm9kZDsgY2xpcC1ydWxlOmV2ZW5vZGQiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGc+PHBhdGggc3R5bGU9Im9wYWNpdHk6MSIgZmlsbD0iIzAwMzM3MCIgZD0iTSA0Mi41LC0wLjUgQyAxMzguNSwtMC41IDIzNC41LC0wLjUgMzMwLjUsLTAuNUMgMzUzLjE2Nyw1LjUgMzY3LjUsMTkuODMzMyAzNzMuNSw0Mi41QyAzNzMuNSwxMzguNSAzNzMuNSwyMzQuNSAzNzMuNSwzMzAuNUMgMzY3LjUsMzUzLjE2NyAzNTMuMTY3LDM2Ny41IDMzMC41LDM3My41QyAyMzQuNSwzNzMuNSAxMzguNSwzNzMuNSA0Mi41LDM3My41QyAxOS44MzMzLDM2Ny41IDUuNSwzNTMuMTY3IC0wLjUsMzMwLjVDIC0wLjUsMjM0LjUgLTAuNSwxMzguNSAtMC41LDQyLjVDIDUuNSwxOS44MzMzIDE5LjgzMzMsNS41IDQyLjUsLTAuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGFlMTNhIiBkPSJNIDE4Ny41LDE0My41IEMgMTk1LjgzMywxNTIuMTY3IDIwNC4xNjcsMTYwLjgzMyAyMTIuNSwxNjkuNUMgMjE5LjcxNCwxODIuMzI2IDIxOC44OCwxOTQuNjU5IDIxMCwyMDYuNUMgMjAyLjI3MiwyMTQuMzk2IDE5NC40MzgsMjIyLjA2MyAxODYuNSwyMjkuNUMgMTY5LjM2OCwyNDcuMTMyIDE1Mi4wMzUsMjY0LjYzMiAxMzQuNSwyODJDIDEyMS45MzIsMjkxLjE4MSAxMDguOTMyLDI5MS44NDggOTUuNSwyODRDIDgyLjU0MzEsMjcyLjQzNCA4MC4wNDMxLDI1OC45MzQgODgsMjQzLjVDIDEwNC4zMDQsMjI2LjE5NSAxMjAuOTcxLDIwOS4xOTUgMTM4LDE5Mi41QyAxNDAuNzg2LDE4OS4wMzMgMTQxLjEyLDE4NS4zNjYgMTM5LDE4MS41QyAxMjIuNjY3LDE2NS4xNjcgMTA2LjMzMywxNDguODMzIDkwLDEzMi41QyA3OS44MjQ5LDExNi40MDYgODEuNjU4MiwxMDEuOTA2IDk1LjUsODlDIDEwOC45Niw4MS4xMTk5IDEyMS45Niw4MS43ODY1IDEzNC41LDkxQyAxNTIuMDM1LDEwOC43MDIgMTY5LjcwMSwxMjYuMjAyIDE4Ny41LDE0My41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwYWUxM2EiIGQ9Ik0gMjc2LjUsMTM5LjUgQyAyNjAuMDA1LDE0Ny4wMzYgMjQ2LjE3MiwxNDMuNzAyIDIzNSwxMjkuNUMgMjI5LjMxMywxMTguODA5IDIyOS4xNDYsMTA4LjE0MyAyMzQuNSw5Ny41QyAyNDYuNjU1LDgyLjEzNyAyNjEuMzIyLDc5LjMwMzcgMjc4LjUsODlDIDI5MS45OTQsMTAxLjMyMiAyOTQuMTYxLDExNS40ODkgMjg1LDEzMS41QyAyODIuMjcxLDEzNC4zOTkgMjc5LjQzOCwxMzcuMDY2IDI3Ni41LDEzOS41IFoiLz48L2c+CjxnPjxwYXRoIHN0eWxlPSJvcGFjaXR5OjEiIGZpbGw9IiMwNDc4NWIiIGQ9Ik0gMjM0LjUsOTcuNSBDIDIyOS4xNDYsMTA4LjE0MyAyMjkuMzEzLDExOC44MDkgMjM1LDEyOS41QyAyNDYuMTcyLDE0My43MDIgMjYwLjAwNSwxNDcuMDM2IDI3Ni41LDEzOS41QyAyNjIuODY5LDE1My42MzIgMjQ5LjAzNSwxNjcuNjMyIDIzNSwxODEuNUMgMjMzLjE4MiwxODQuNDE2IDIzMy4wMTYsMTg3LjQxNiAyMzQuNSwxOTAuNUMgMjI3LjE2NywxODMuNSAyMTkuODMzLDE3Ni41IDIxMi41LDE2OS41QyAyMDQuMTY3LDE2MC44MzMgMTk1LjgzMywxNTIuMTY3IDE4Ny41LDE0My41QyAyMDMuMTQsMTI4LjE5NCAyMTguODA3LDExMi44NjEgMjM0LjUsOTcuNSBaIi8+PC9nPgo8Zz48cGF0aCBzdHlsZT0ib3BhY2l0eToxIiBmaWxsPSIjMGRiNDBiIiBkPSJNIDIxMi41LDE2OS41IEMgMjE5LjgzMywxNzYuNSAyMjcuMTY3LDE4My41IDIzNC41LDE5MC41QyAyNTEuMTMyLDIwNy42MzIgMjY3Ljk2NSwyMjQuNjMyIDI4NSwyNDEuNUMgMjk0LjA2NywyNTcuNDIgMjkxLjkwMSwyNzEuNTg3IDI3OC41LDI4NEMgMjY3LjA1NSwyOTAuOTM1IDI1NS4zODksMjkxLjI2OSAyNDMuNSwyODVDIDIyNC45MzYsMjY1LjkzNCAyMDUuOTM2LDI0Ny40MzQgMTg2LjUsMjI5LjVDIDE5NC40MzgsMjIyLjA2MyAyMDIuMjcyLDIxNC4zOTYgMjEwLDIwNi41QyAyMTguODgsMTk0LjY1OSAyMTkuNzE0LDE4Mi4zMjYgMjEyLjUsMTY5LjUgWiIvPjwvZz4KPC9zdmc+Cg==", icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI2LjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA0MjggNDI4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjggNDI4OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMTI1LjgsMjQzLjdjMTIuMywwLDI0LjMtNC4xLDM0LTExLjZsLTc4LTc4Yy0xOC44LDI0LjMtMTQuMyw1OS4zLDEwLDc4LjEKCUMxMDEuNiwyMzkuNiwxMTMuNSwyNDMuNywxMjUuOCwyNDMuN0wxMjUuOCwyNDMuN3oiLz4KPHBhdGggc3R5bGU9ImZpbGw6IzAwMTkzQzsiIGQ9Ik0zNTcuOCwxODhjMC0xMi4zLTQuMS0yNC4zLTExLjYtMzRsLTc4LDc4YzI0LjMsMTguOCw1OS4yLDE0LjMsNzgtMTAKCUMzNTMuNywyMTIuMywzNTcuOCwyMDAuMywzNTcuOCwxODh6Ii8+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMzk3LjEsMTAzLjFsLTM0LjUsMzQuNWMyNy44LDMzLjMsMjMuNCw4Mi45LTkuOSwxMTAuN2MtMjkuMiwyNC40LTcxLjYsMjQuNC0xMDAuOCwwTDIxNCwyODYuMgoJbC0zNy44LTM3LjhjLTMzLjMsMjcuOC04Mi45LDIzLjQtMTEwLjctOS45Yy0yNC40LTI5LjItMjQuNC03MS42LDAtMTAwLjhMNDcuOCwxMjBMMzEsMTAzLjFDMTAuNywxMzYuNSwwLDE3NC45LDAsMjE0CgljMCwxMTguMiw5NS44LDIxNCwyMTQsMjE0czIxNC05NS44LDIxNC0yMTRDNDI4LjEsMTc0LjksNDE3LjMsMTM2LjUsMzk3LjEsMTAzLjF6Ii8+CjxwYXRoIHN0eWxlPSJmaWxsOiMwMDE5M0M7IiBkPSJNMzY4LjgsNjYuM2MtODEuNS04NS41LTIxNi45LTg4LjctMzAyLjQtNy4yYy0yLjUsMi40LTQuOSw0LjgtNy4yLDcuMmMtNS4zLDUuNi0xMC4zLDExLjQtMTUsMTcuNQoJTDIxNCwyNTMuN0wzODMuOCw4My44QzM3OS4yLDc3LjcsMzc0LjEsNzEuOSwzNjguOCw2Ni4zeiBNMjE0LDI4YzUwLDAsOTYuNiwxOS4zLDEzMS42LDU0LjVMMjE0LDIxNC4xTDgyLjQsODIuNQoJQzExNy40LDQ3LjMsMTY0LDI4LDIxNCwyOHoiLz4KPC9zdmc+Cg==", testnet: false, wrappedToken: "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1", diff --git a/src/mpcContract.ts b/src/mpcContract.ts index 7f020a5..b264c54 100644 --- a/src/mpcContract.ts +++ b/src/mpcContract.ts @@ -43,7 +43,7 @@ interface MpcContractInterface extends Contract { * High-level interface for the Near MPC-Recovery Contract * located in: https://github.com/near/mpc-recovery */ -export class MpcContract { +export class MpcContract implements IMpcContract { contract: MpcContractInterface; connectedAccount: Account; @@ -142,3 +142,15 @@ function gasOrDefault(gas?: bigint): string { // Default of 250 TGAS return (TGAS * 250n).toString(); } + +export interface IMpcContract { + connectedAccount: Account; + accountId(): string; + deriveEthAddress(derivationPath: string): Promise
; + getDeposit(): Promise; + requestSignature(signArgs: SignArgs, gas?: bigint): Promise; + encodeSignatureRequestTx( + signArgs: SignArgs, + gas?: bigint + ): Promise>; +} diff --git a/src/types.ts b/src/types.ts index dedcf6c..036eb2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { MpcContract } from "./mpcContract"; +import { IMpcContract } from "./mpcContract"; import { Address, Hex, @@ -48,7 +48,7 @@ export interface BaseTx { * @property {string} [derivationPath] - Path used to generate ETH account from NEAR account (e.g., "ethereum,1"). */ export interface AdapterParams { - mpcContract: MpcContract; + mpcContract: IMpcContract; derivationPath?: string; } diff --git a/src/utils/index.ts b/src/utils/index.ts index e756f28..56df95a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ export * from "./request"; export * from "./signature"; export * from "./transaction"; + +export { mockAdapter } from "./mock-sign"; diff --git a/src/utils/mock-sign.ts b/src/utils/mock-sign.ts new file mode 100644 index 0000000..0c80fb2 --- /dev/null +++ b/src/utils/mock-sign.ts @@ -0,0 +1,106 @@ +import { Address, Hex, Signature, toHex } from "viem"; +import { PrivateKeyAccount, privateKeyToAccount } from "viem/accounts"; +import { FunctionCallTransaction, SignArgs } from "../types"; +import { Account } from "near-api-js"; +import { IMpcContract, nearAccountFromAccountId, NearEthAdapter } from ".."; + +function fromPayload(payload: number[]): Hex { + if (payload.length !== 32) { + throw new Error(`Payload must have 32 bytes: ${payload}`); + } + // Convert number[] back to Uint8Array + return toHex(new Uint8Array(payload)); +} + +/** + * Converts a raw hexadecimal signature into a structured Signature object + * @param hexSignature The raw hexadecimal signature (e.g., '0x...') + * @returns A structured Signature object with fields r, s, v, and yParity + */ +function hexToSignature(hexSignature: Hex): Signature { + // Strip "0x" prefix if it exists + const cleanedHex = hexSignature.slice(2); + + // Ensure the signature is 65 bytes (130 hex characters) + if (cleanedHex.length !== 130) { + throw new Error( + `Invalid hex signature length: ${cleanedHex.length}. Expected 130 characters (65 bytes).` + ); + } + + // Extract the r, s, and v components from the hex signature + const v = BigInt(`0x${cleanedHex.slice(128, 130)}`); // Last byte (2 hex characters) + return { + r: `0x${cleanedHex.slice(0, 64)}`, // First 32 bytes (64 hex characters) + s: `0x${cleanedHex.slice(64, 128)}`, // Next 32 bytes (64 hex characters), + v, + // Determine yParity based on v (27 or 28 maps to 0 or 1) + yParity: v === 27n ? 0 : v === 28n ? 1 : undefined, + }; +} + +export class MockMpcContract implements IMpcContract { + connectedAccount: Account; + private ethAccount: PrivateKeyAccount; + + constructor(account: Account, privateKey?: Hex) { + this.connectedAccount = account; + this.ethAccount = privateKeyToAccount( + privateKey || + // Known key from deterministic ganache client + "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" + ); + } + + accountId(): string { + return "mock-mpc.offline"; + } + + deriveEthAddress = async (_unused?: string): Promise
=> { + return this.ethAccount.address; + }; + + getDeposit = async (): Promise => { + return "1"; + }; + + requestSignature = async ( + signArgs: SignArgs, + _gas?: bigint + ): Promise => { + const hexSignature = await this.ethAccount.sign({ + hash: fromPayload(signArgs.payload), + }); + return hexToSignature(hexSignature); + }; + + async encodeSignatureRequestTx( + signArgs: SignArgs, + gas?: bigint + ): Promise> { + return { + signerId: this.connectedAccount.accountId, + receiverId: this.accountId(), + actions: [ + { + type: "FunctionCall", + params: { + methodName: "sign", + args: { request: signArgs }, + gas: gas ? gas.toString() : "1", + deposit: await this.getDeposit(), + }, + }, + ], + }; + } +} + +export async function mockAdapter(privateKey?: Hex): Promise { + const account = await nearAccountFromAccountId("mock-user.offline", { + networkId: "testnet", + nodeUrl: "https://rpc.testnet.near.org", + }); + const mpcContract = new MockMpcContract(account, privateKey); + return NearEthAdapter.fromConfig({ mpcContract }); +} diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 2af65dd..114fd44 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -1,6 +1,9 @@ -import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "../examples/setup"; -import { NearEthAdapter, Network } from "../src"; +import { SEPOLIA_CHAIN_ID } from "../examples/setup"; +import { mockAdapter, NearEthAdapter, Network } from "../src"; import { getBalance } from "viem/actions"; +import dotenv from "dotenv"; +import { recoverTypedDataAddress, recoverMessageAddress } from "viem"; +dotenv.config(); describe("End To End", () => { let adapter: NearEthAdapter; @@ -9,7 +12,10 @@ describe("End To End", () => { const chainId = SEPOLIA_CHAIN_ID; beforeAll(async () => { - adapter = await setupNearEthAdapter(); + // This is a real adapter. + // adapter = await setupNearEthAdapter(); + // This is a Mock Adapter! + adapter = await mockAdapter(process.env.ETH_PK! as `0x${string}`); }); afterAll(async () => { @@ -20,7 +26,7 @@ describe("End To End", () => { await expect(adapter.getBalance(chainId)).resolves.not.toThrow(); }); - it.only("signAndSendTransaction", async () => { + it.skip("signAndSendTransaction", async () => { await expect( adapter.signAndSendTransaction({ // Sending 1 WEI to self (so we never run out of funds) @@ -34,7 +40,7 @@ describe("End To End", () => { it.skip("signAndSendTransaction - Gnosis Chain", async () => { await expect( adapter.signAndSendTransaction({ - // Sending 1 WEI to self (so we never run out of funds) + // Sending 1 WEI to self (so we ~never run out of funds) to: adapter.address, value: ONE_WEI, // Gnosis Chain! @@ -58,7 +64,13 @@ describe("End To End", () => { }); it("signMessage", async () => { - await expect(adapter.signMessage("NearEth")).resolves.not.toThrow(); + const message = "NearEth"; + const signature = await adapter.signMessage(message); + const recoveredAddress = await recoverMessageAddress({ + message, + signature, + }); + expect(recoveredAddress).toBe(adapter.address); }); it("signTypedData", async () => { @@ -93,13 +105,18 @@ describe("End To End", () => { ], } as const; - await expect( - adapter.signTypedData({ - types, - primaryType: "Mail", - message, - domain, - }) - ).resolves.not.toThrow(); + const typedData = { + types, + primaryType: "Mail", + message, + domain, + } as const; + const signature = await adapter.signTypedData(typedData); + + const recoveredAddress = await recoverTypedDataAddress({ + ...typedData, + signature, + }); + expect(recoveredAddress).toBe(adapter.address); }); }); diff --git a/tests/unit/utils.mock-sign.test.ts b/tests/unit/utils.mock-sign.test.ts new file mode 100644 index 0000000..59868ba --- /dev/null +++ b/tests/unit/utils.mock-sign.test.ts @@ -0,0 +1,22 @@ +import { recoverMessageAddress } from "viem"; +import { mockAdapter } from "../../src/utils/mock-sign"; + +describe("Mock Signing", () => { + it("MockAdapter", async () => { + const adapter = await mockAdapter(); + expect(adapter.address).toBe("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"); + + const message = "Hello Joe!"; + const signature = await adapter.signMessage(message); + + expect(signature).toBe( + "0xcadb9d3ade67e815c11646eea6cd52abb7f860af612cd914a2c64c01908af870246926e8d5774d0b63efc46cd9f77cc650a7ad923df69a5151805422ab1d625a1c" + ); + // Recover Address: + const recoveredAddress = await recoverMessageAddress({ + message, + signature, + }); + expect(recoveredAddress).toBe(adapter.address); + }); +});