From c0b6d2475d5b2b5b184cae81dbd1bdb80ef7ecc0 Mon Sep 17 00:00:00 2001 From: frankcrypto Date: Tue, 20 Aug 2024 15:35:13 +0800 Subject: [PATCH] debug export balance from utxo to evm --- packages/site/src/components/Card.tsx | 117 ++++++++++- packages/snap/snap.manifest.json | 2 +- packages/snap/src/getAbstractAccount.ts | 9 +- packages/snap/src/index.ts | 42 +++- packages/snap/src/qng.ts | 63 +++++- packages/snap/src/qngweb3.ts | 266 +++++++++++++++++------- 6 files changed, 413 insertions(+), 86 deletions(-) diff --git a/packages/site/src/components/Card.tsx b/packages/site/src/components/Card.tsx index 897a4439..fd6741cc 100644 --- a/packages/site/src/components/Card.tsx +++ b/packages/site/src/components/Card.tsx @@ -103,7 +103,12 @@ export const AACard = () => { const [address, setAddress] = useState(''); const [balance, setBalance] = useState(''); const [qngAddress, setQngAddress] = useState(''); + const [qngBalance, setQngBalance] = useState(''); const [target, setTarget] = useState(''); + const [txide, setTxid] = useState(''); + const [idx, setIdx] = useState(''); + const [fee, setFee] = useState('10000'); + const [oneUtxo, setOneUtxo] = useState(''); const [ethAmount, setEthAmount] = useState(''); // const Hash160 = (pubHex: string): string => { @@ -118,6 +123,13 @@ export const AACard = () => { setAddress((await invokeSnap({ method: 'connect' })) as string); setQngAddress((await invokeSnap({ method: 'connect_qng' })) as string); setBalance((await invokeSnap({ method: 'balance' })) as string); + setQngBalance((await invokeSnap({ method: 'balance_qng' })) as string); + setOneUtxo( + (await invokeSnap({ + method: 'getOneUtxo', + params: { utxoFrom: qngAddress }, + })) as string, + ); } catch (er) { console.error(er); } @@ -127,6 +139,13 @@ export const AACard = () => { try { setEOABalance((await invokeSnap({ method: 'balance_eoa' })) as string); setBalance((await invokeSnap({ method: 'balance' })) as string); + setQngBalance((await invokeSnap({ method: 'balance_qng' })) as string); + setOneUtxo( + (await invokeSnap({ + method: 'getOneUtxo', + params: { utxoFrom: qngAddress }, + })) as string, + ); } catch (er) { console.error(er); } @@ -140,6 +159,18 @@ export const AACard = () => { setEthAmount(ev.currentTarget.value); }; + const handleTxidChange = (ev: React.FocusEvent) => { + setTxid(ev.currentTarget.value); + }; + + const handleIdxChange = (ev: React.FocusEvent) => { + setIdx(ev.currentTarget.value); + }; + + const handleFeeChange = (ev: React.FocusEvent) => { + setFee(ev.currentTarget.value); + }; + const handleTransferFromAAClick = async () => { if (!target || !ethAmount) { // eslint-disable-next-line no-alert @@ -173,7 +204,7 @@ export const AACard = () => { try { const ethValue = `${ethAmount}`; - const rawTx = (await invokeSnap({ + const txid = (await invokeSnap({ method: 'utxoTransfer', params: { from: qngAddress, @@ -183,12 +214,62 @@ export const AACard = () => { })) as string; // eslint-disable-next-line no-alert alert(`tx sign succ`); - console.log(rawTx); + console.log(txid); + } catch (er) { + console.error(er); + } + }; + + const handleUtxoToEvmClick = async () => { + if (!txide || !idx) { + // eslint-disable-next-line no-alert + alert('enter txid and idx and fee.'); + return; + } + + const withWallet = false; + try { + const res = (await invokeSnap({ + method: 'export', + params: { + txid: txide, + idx, + fee, + withWallet, + }, + })) as string; + // eslint-disable-next-line no-alert + alert(`sign succ ${res}`); + console.log(res); } catch (er) { console.error(er); } }; + const handleUtxoToEvmWithWalletClick = async () => { + if (!txide || !idx) { + // eslint-disable-next-line no-alert + alert('enter txid and idx and fee.'); + return; + } + const withWallet = true; + try { + const res = (await invokeSnap({ + method: 'export', + params: { + txid: txide, + idx, + fee, + withWallet, + }, + })) as string; + // eslint-disable-next-line no-alert + alert(`sign succ ${res}`); + console.log(res); + } catch (er) { + console.error(er); + } + }; return ( <> {!address && ( @@ -215,6 +296,7 @@ export const AACard = () => { Your Qng Account 🎉 P2KH Address: {qngAddress} + Balance: {qngBalance} @@ -258,6 +340,37 @@ export const AACard = () => { Transfer from UTXO + + + Transfer from Qng Account +
+ + + +
+ Available UTXO of the EoaAddress + {oneUtxo} + + + + +
)} diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index f0e24d24..384c635e 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/template-snap-monorepo.git" }, "source": { - "shasum": "X0E7Z4Xx8kFeadQjs1fMG9MNX51YoJSBtbUo+Ok+VJ8=", + "shasum": "cUuESUJvxaTB3CM5yTCgZYfD6R1mZzWVl08DxsyWJyA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/getAbstractAccount.ts b/packages/snap/src/getAbstractAccount.ts index 531d568b..52490a47 100644 --- a/packages/snap/src/getAbstractAccount.ts +++ b/packages/snap/src/getAbstractAccount.ts @@ -92,7 +92,14 @@ const entryPointAddress = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'; // deployed by deterministic-deployment-proxy https://github.com/Arachnid/deterministic-deployment-proxy.git const factoryAddress = '0x9406cc6185a346906296840746125a0e44976454'; // const paymasterUrl = ''; // Optional -const bundlerUrl = 'http://127.0.0.1:3000/rpc'; + +// ** server response header set "Access-Control-Allow-Origin": "null" ** +export const proxyUrl = 'http://127.0.0.1:8081'; +export const bundlerUrl = `${proxyUrl}/bundler`; +export const qngUrl = `${proxyUrl}/qng`; + +// TODO crossQngUrl will be merged in bundlerUrl +export const crossQngUrl = `${proxyUrl}/export`; export const getAbstractAccount = async (): Promise => { const provider = new ethers.providers.Web3Provider(ethereum as any); await provider.send('eth_requestAccounts', []); diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index d1526f28..b1533378 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -4,7 +4,14 @@ import { ethers } from 'ethers'; import { getAbstractAccount } from './getAbstractAccount'; import { getBalance } from './getBalance'; -import { getQngAddress, qngTransfer } from './qng'; +import { + getQngAddress, + qngTransfer, + getQngBalance, + ethSign, + walletSign, + getOneUtxo, +} from './qng'; import { transfer } from './transfer'; // export const changeNetwork = async () => { // await ethereum.request({ @@ -47,6 +54,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ return await getQngAddress(); case 'balance_eoa': return await getBalance(await getEoaAddress()); + case 'balance_qng': + return await getQngBalance(); case 'connect': return await getAddress(); case 'balance': @@ -63,6 +72,37 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ [key: string]: string; }; return await qngTransfer(from as string, to as string, amount as string); + case 'export': + // eslint-disable-next-line no-case-declarations + const { txid, idx, fee, withWallet } = request?.params as unknown as { + [key: string]: any; + }; + if (!withWallet) { + return await ethSign(txid as string, idx as number, fee as number); + } + // eslint-disable-next-line no-case-declarations + const res = await snap.request({ + method: 'snap_dialog', + params: { + type: 'confirmation', + content: panel([ + text(`sign with 813 wallet`), + // eslint-disable-next-line no-template-curly-in-string, @typescript-eslint/restrict-template-expressions + text(`Sign Content\n txid:${txid} \nidx:${idx} \nfee:${fee}`), + text('Please Check the fee!'), + ]), + }, + }); + if (!res) { + return ''; + } + return walletSign(txid as string, idx as number, fee as number); + case 'getOneUtxo': + // eslint-disable-next-line no-case-declarations + const { utxoFrom } = request?.params as unknown as { + [key: string]: string; + }; + return getOneUtxo(utxoFrom as string); case 'hello': return snap.request({ method: 'snap_dialog', diff --git a/packages/snap/src/qng.ts b/packages/snap/src/qng.ts index c16feebe..53a82227 100644 --- a/packages/snap/src/qng.ts +++ b/packages/snap/src/qng.ts @@ -1,11 +1,18 @@ import { SLIP10Node } from '@metamask/key-tree'; +import { ethers } from 'ethers'; import { ec, hash, address, networks } from 'qitmeerts'; import * as uint8arraytools from 'uint8array-tools'; -import { qngTransferUtxo } from './qngweb3'; - -export const trimHexPrefix = (key: string) => - key.startsWith('0x') ? key.substring(2) : key; +import { + qngTransferUtxo, + qngGetUTXOBalance, + transferUTXOToEvmWithEthSign, + getInputHash, + qngGetAvailableUtxos, + crossSendtoBunder, + handleSignStr, + trimHexPrefix, +} from './qngweb3'; export const CRYPTO_CURVE = 'secp256k1'; @@ -35,10 +42,48 @@ export const getQngAddress = async (): Promise => { ); const pub = privKey.publicKey; const h16 = hash.hash160(pub); - const addr = address.toBase58Check(h16, networks.testnet.pubKeyHashAddrId); + const addr = address.toBase58Check(h16, networks.privnet.pubKeyHashAddrId); return addr; }; +export const getQngBalance = async (): Promise => { + const addr = await getQngAddress(); + const ba = await qngGetUTXOBalance(addr); + return ba; +}; + +export const ethSign = async ( + txid: string, + idx: number, + fee: number, +): Promise => { + const signRes = await transferUTXOToEvmWithEthSign(txid, idx, fee); + return signRes; +}; + +export const walletSign = async ( + txid: string, + idx: number, + fee: number, +): Promise => { + const account = await getQngAccount(); + const privKey = ec.fromPrivateKey( + uint8arraytools.fromHex(trimHexPrefix(account.privateKey as string)), + {}, + ); + const wallet = new ethers.Wallet( + uint8arraytools.toHex(privKey.privateKey as Uint8Array), + ); + const signature = await wallet.signMessage(getInputHash(txid, idx, fee)); + const txhash = await crossSendtoBunder( + txid, + idx, + fee, + handleSignStr(signature), + ); + return txhash; +}; + export const qngTransfer = async ( _from: string, _target: string, @@ -53,3 +98,11 @@ export const qngTransfer = async ( const txid = qngTransferUtxo(_from, _target, Number(_amount), privKey); return txid; }; +export const getOneUtxo = async (from: string): Promise => { + const last = await qngGetAvailableUtxos(from); + if (last.length < 1) { + return ''; + } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${last[0]?.txid}:${last[0]?.idx}:${last[0]?.amount}`; +}; diff --git a/packages/snap/src/qngweb3.ts b/packages/snap/src/qngweb3.ts index 51605ee5..4229ba1a 100644 --- a/packages/snap/src/qngweb3.ts +++ b/packages/snap/src/qngweb3.ts @@ -2,10 +2,58 @@ import { ethers } from 'ethers'; import { TxSigner as txsign, networks } from 'qitmeerts'; import * as uint8arraytools from 'uint8array-tools'; +import { qngUrl, crossQngUrl } from './getAbstractAccount'; +// ** server response header set "Access-Control-Allow-Origin": "null" ** + +export const trimHexPrefix = (key: string) => + key.startsWith('0x') ? key.substring(2) : key; +const _qngSend = async (method: string, params: any): Promise => { + const body = { + jsonrpc: '2.0', + method, + params, + id: 1, + }; + const re = await fetch(qngUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + const result = await re.json(); + try { + return result.result; + } catch (error) { + return ''; + } +}; +const _qngCrossSend = async (method: string, params: any): Promise => { + const body = { + jsonrpc: '2.0', + method, + params, + id: 1, + }; + // send to bundler + const re = await fetch(crossQngUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + const result = await re.json(); + try { + return result.result; + } catch (error) { + return ''; + } +}; // jsonrpc Request // { // "jsonrpc": "2.0", -// "method": "qng_getUTXOBalance", +// "method": "qng_addBalance", // "params": [ // "TnJdESQUPsA9mLrVf2NWRXiobdMUoCBH3h8" // ], @@ -17,33 +65,64 @@ import * as uint8arraytools from 'uint8array-tools'; // "error": { // "code": 0, // "message": "ok", -// "data": '100000000' +// "result": null // }, // "id": 1 // } -export const qngGetUTXOBalance = async (addr: string): Promise => { - const provider = new ethers.providers.Web3Provider(ethereum as any); - // const httpProvider = new ethers.providers.JsonRpcProvider(); - await provider.getNetwork(); - const params = [addr]; - const result = await provider.send('qng_getUTXOBalance', params); +export const qngAddWatchAddr = async (addr: string): Promise => { try { - return result; + await _qngSend('qng_addBalance', [addr]); } catch (error) { - return '100000000'; + console.log(error); + } +}; + +// jsonrpc Request +// { +// "jsonrpc": "2.0", +// "method": "qng_getBalance", +// "params": [ +// "TnJdESQUPsA9mLrVf2NWRXiobdMUoCBH3h8" +// ], +// "id": 1 +// } +// jsonrpc Response +// { +// "jsonrpc": "2.0", +// "error": { +// "code": 0, +// "message": "ok", +// "result": 100000000 +// }, +// "id": 1 +// } +export const qngGetUTXOBalance = async (addr: string): Promise => { + // const provider = new ethers.providers.Web3Provider(ethereum as any); + // await provider.getNetwork(); + // const httpProvider = new ethers.providers.JsonRpcProvider(RPC_API); + // const params = [addr, 0]; + // const result = await httpProvider.send('qng_getBalance', params); + const ba = await _qngSend('qng_getBalance', [addr, 0]); + try { + return (ba as number).toString(); + } catch (error: any) { + console.log(error); + return error.message as string; } }; type UTXO = { - txid: string; - outputindex: number; + type: string; amount: number; + txid: string; + idx: number; + status: string; }; // jsonrpc Request // { // "jsonrpc": "2.0", -// "method": "qng_getAvaliableUTXOs", +// "method": "qng_getUTXOs", // "params": [ // "TnJdESQUPsA9mLrVf2NWRXiobdMUoCBH3h8" // ], @@ -55,20 +134,21 @@ type UTXO = { // "error": { // "code": 0, // "message": "ok", -// "data": [{"txid":"13243545abcd232432545...","outputindex":1,"amount":10000000000},{"txid":"e123243545abcd23243254568...","outputindex":3,"amount":20000000000}] +// "result": [{"txid":"13243545abcd232432545...","idx":1,"amount":10000000000},{"txid":"e123243545abcd23243254568...","idx":3,"amount":20000000000}] // }, // "id": 1 // } export const qngGetAvailableUtxos = async (addr: string): Promise => { - const provider = new ethers.providers.Web3Provider(ethereum as any); - // const httpProvider = new ethers.providers.JsonRpcProvider(); - await provider.getNetwork(); - const params = [addr]; - const result = await provider.send('qng_getAvaliableUTXOs', params); + // const provider = new ethers.providers.Web3Provider(ethereum as any); + // // const httpProvider = new ethers.providers.JsonRpcProvider(); + // await provider.getNetwork(); + const params = [addr, 50, false]; + const ret = await _qngSend('qng_getUTXOs', params); try { - return result as UTXO[]; + return ret as UTXO[]; } catch (error) { - return [] as UTXO[]; + console.log(error); + return []; } }; @@ -87,7 +167,7 @@ export const qngGetAvailableUtxos = async (addr: string): Promise => { // "error": { // "code": 0, // "message": "ok", -// "data": "23243546123243546feadc43546546123243546123243546" +// "result": "23243546123243546feadc43546546123243546123243546" // }, // "id": 1 // } @@ -99,10 +179,46 @@ export const sendRawTx = async ( // const httpProvider = new ethers.providers.JsonRpcProvider(); await provider.getNetwork(); const params = [uint8arraytools.toHex(rawData), allowHighFee]; - const result = await provider.send('qng_sendRawTx', params); try { - return result; + const ret = await _qngSend('qng_sendRawTransaction', params); + return ret as string; + } catch (error) { + console.log(error); + return ''; + } +}; + +// jsonrpc Request +// { +// "jsonrpc": "2.0", +// "method": "qng_crossSend", +// "params": [ +// "123243546123243546feadc435465461232435461232435",0,10000,"2324356786779feab43677" +// ], +// "id": 1 +// } +// jsonrpc Response +// { +// "jsonrpc": "2.0", +// "error": { +// "code": 0, +// "message": "ok", +// "result": "23243546123243546feadc43546546123243546123243546" +// }, +// "id": 1 +// } +export const crossSendtoBunder = async ( + txid: string, + idx: number, + fee: number, + sign: string, +): Promise => { + const params = [txid, idx, fee, sign]; + try { + const ret = await _qngCrossSend('qng_crossSend', params); + return ret as string; } catch (error) { + console.log(error); return ''; } }; @@ -139,16 +255,24 @@ export const qngTransferUtxo = async ( amount: number, privKey: any, ): Promise => { - const needUtxos = await qngGetTransferUtxos(from, amount); - const txsnr = txsign.newSigner(networks.testnet); + const fee = 10000; + let leftAmount: number = amount + fee; + const needUtxos = await qngGetTransferUtxos(from, leftAmount); + const txsnr = txsign.newSigner(networks.privnet); txsnr.setTimestamp(parseInt(`${(new Date() as any) / 1000}`, 10)); txsnr.setVersion(1); + let allInputAmount = 0; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < needUtxos.length; i++) { const utxo = needUtxos[i] as UTXO; - txsnr.addInput(utxo.txid, utxo.outputindex); + txsnr.addInput(utxo.txid, utxo.idx); + allInputAmount += utxo.amount; } txsnr.addOutput(to, amount); + leftAmount = allInputAmount - amount - fee; + if (leftAmount > 0) { + txsnr.addOutput(from, leftAmount); + } // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < needUtxos.length; i++) { txsnr.sign(i, privKey); @@ -158,58 +282,48 @@ export const qngTransferUtxo = async ( return sendRawTx(rawTx, false); }; -// UTXO To EVM -export const transferUTXOToEvm = async ( - from: string, - to: string, - amount: number, - privKey: any, -): Promise => { - const needUtxos = await qngGetTransferUtxos(from, amount); - const txsnr = txsign.newSigner(networks.testnet); - txsnr.setTimestamp(parseInt(`${(new Date() as any) / 1000}`, 10)); - txsnr.setVersion(1); - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < needUtxos.length; i++) { - const utxo = needUtxos[i] as UTXO; - txsnr.addInput(utxo.txid, utxo.outputindex); +export const getInputHash = ( + txid: string, + idx: number, + fee: number, +): Uint8Array => { + const re = new Uint8Array(44); + const constTxId: Uint8Array = uint8arraytools.fromHex(txid); + re.set(constTxId.reverse(), 0); + uint8arraytools.writeUInt32(re, 32, idx, 'BE'); + uint8arraytools.writeUInt64(re, 36, BigInt(fee), 'BE'); + return re; +}; + +export const handleSignStr = (sign: string): string => { + const lastTwoChars = sign.slice(-2); + let newStr = sign; + // eth sign v is 27 or 28 + // golang sign v is 0 or 1 + if (lastTwoChars === '1b') { + newStr = `${sign.slice(0, -2)}00`; } - txsnr.addOutput(to, amount); - // eslint-disable-next-line @typescript-eslint/prefer-for-of - // TODO need sign different type like qx - for (let i = 0; i < needUtxos.length; i++) { - // txsnr.signExport(i, privKey); - txsnr.sign(i, privKey); + if (lastTwoChars === '1c') { + newStr = `${sign.slice(0, -2)}01`; } - // get raw Tx - const rawTx = txsnr.build().toBuffer(); - return sendRawTx(rawTx, false); + return trimHexPrefix(newStr); }; -// EVM To UTXO -export const qngTransferEvmToUtxo = async ( - from: string, - to: string, - amount: number, - privKey: any, +// UTXO To EVM +export const transferUTXOToEvmWithEthSign = async ( + txid: string, + idx: number, + fee: number, ): Promise => { - const needUtxos = await qngGetTransferUtxos(from, amount); - const txsnr = txsign.newSigner(networks.testnet); - txsnr.setTimestamp(parseInt(`${(new Date() as any) / 1000}`, 10)); - txsnr.setVersion(1); - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < needUtxos.length; i++) { - const utxo = needUtxos[i] as UTXO; - txsnr.addInput(utxo.txid, utxo.outputindex); - } - txsnr.addOutput(to, amount); - // eslint-disable-next-line @typescript-eslint/prefer-for-of - // TODO need sign different type like qx - for (let i = 0; i < needUtxos.length; i++) { - // txsnr.signImport(i, privKey); - txsnr.sign(i, privKey); - } - // get raw Tx - const rawTx = txsnr.build().toBuffer(); - return sendRawTx(rawTx, false); + const ret = uint8arraytools.toHex(getInputHash(txid, idx, fee)); + + const provider = new ethers.providers.Web3Provider(ethereum as any); + // auth user + const accounts = await provider.send('eth_requestAccounts', []); + const sign = (await ethereum.request({ + method: 'personal_sign', + params: [ret, accounts[0]], + })) as string; + const txhash = crossSendtoBunder(txid, idx, fee, handleSignStr(sign)); + return txhash; };