diff --git a/src/api/RecordApi.ts b/src/api/RecordApi.ts new file mode 100644 index 0000000..b6ad5fc --- /dev/null +++ b/src/api/RecordApi.ts @@ -0,0 +1,17 @@ +import { WALLET_API_HOST } from "../config/constants"; +import { DELETE_JSON, POST_JSON } from "../lib/Http"; + +export function saveRecord(data) { + const url = `${WALLET_API_HOST}/trans/record`; + return POST_JSON(url, data); +} + +export function records(data) { + const url = `${WALLET_API_HOST}/trans/records`; + return POST_JSON(url, data); +} + +export function removeRecord(data) { + const url = `${WALLET_API_HOST}/trans/record`; + return DELETE_JSON(url, data); +} diff --git a/src/common/AsyncQueue.ts b/src/common/AsyncQueue.ts new file mode 100644 index 0000000..792c27e --- /dev/null +++ b/src/common/AsyncQueue.ts @@ -0,0 +1,107 @@ +type Callback = () => Promise + +export type AsyncQueue = { + push: (task: Callback) => Promise + flush: () => Promise + size: number +} + +/** + * Ensures that each callback pushed onto the queue is executed in series. + * Such a quetie 😻 + * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple + * tasks are pushed onto the queue while there is an active task, only the + * last one will be executed, once the active task has completed. + * e.g. in the below example, only 0 and 3 will be executed. + * ``` + * const queue = createAsyncQueue({ dedupeConcurrent: true }) + * queue.push(async () => console.log(0)) // returns 0 + * queue.push(async () => console.log(1)) // returns 3 + * queue.push(async () => console.log(2)) // returns 3 + * queue.push(async () => console.log(3)) // returns 3 + * ``` + * */ +export function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue { + const { dedupeConcurrent } = opts + let queue: Callback[] = [] + let running: Promise | undefined + let nextPromise = new DeferredPromise() + const push = (task: Callback) => { + let taskPromise = new DeferredPromise() + if (dedupeConcurrent) { + queue = [] + if (nextPromise.started) nextPromise = new DeferredPromise() + taskPromise = nextPromise + } + queue.push(() => { + taskPromise.started = true + task().then(taskPromise.resolve).catch(taskPromise.reject) + return taskPromise.promise + }) + if (!running) running = start() + return taskPromise.promise + } + const start = async () => { + while (queue.length) { + const task = queue.shift()! + await task().catch(() => {}) + } + running = undefined + } + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length + }, + } +} + +export const createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues: { [queueId: string]: AsyncQueue } = {} + const push = (queueId: string, task: Callback) => { + if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts) + return queues[queueId].push(task) + } + const flush = (queueId: string) => { + if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts) + return queues[queueId].flush() + } + return { push, flush } +} + +class DeferredPromise { + started = false + resolve: (x: T | PromiseLike) => void = () => {} + reject: (x: E) => void = () => {} + promise: Promise + + constructor() { + this.promise = new Promise((res, rej) => { + this.resolve = res + this.reject = rej + }) + } +} + +// function main() { +// const queue = createAsyncQueue() +// queue.push(async () => { +// console.log(0) +// }) // returns 0 +// queue.push(async () => { +// console.log(1) + +// return new Promise((resolve, reject) => { +// setTimeout(() => { +// console.log('12') +// resolve() +// }, 1000) +// }) +// }) // returns 3 +// queue.push(async () => console.log(2)) // returns 3 +// queue.push(async () => console.log(3)) // returns 3 +// console.log('hi') +// } + +// main() diff --git a/src/config/constants.ts b/src/config/constants.ts index b3252c9..ea88e37 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -5,3 +5,7 @@ export const WALLET_API_HOST = "https://wallet.cebggame.com"; export const MAX_TRY_COUNT = 6; export const MAX_UPLOAD_COUNT = 10; + +export const TX_CONFIRM_BLOCKS = 6; + +export const NATIVE_PK_PREFIX = "0x000000000000000000000000" diff --git a/src/index.ts b/src/index.ts index 16fd2a4..f7abf5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,46 @@ -import { singleton } from "./decorator/singleton.decorator"; -import Web3 from "web3"; import { recoverTypedSignature, signTypedData, SignTypedDataVersion, } from "@metamask/eth-sig-util"; +import Web3 from "web3"; import "whatwg-fetch"; -import { AllChains } from "./data/allchain"; import { createWalletEvents, WALLET_ACCOUNT_CHANGE, WALLET_CHAIN_CHANGE, WALLET_TOKEN_TYPE_CHANGE, } from "./common/WalletEvent"; -import { ERC20Standard } from "./standards/ERC20Standard"; -import { ERC721Standard } from "./standards/ERC721Standard"; -import { IAccount, INFT, initAccount, initNFT } from "./data/DataModel"; -import { loadToken, saveData } from "./manage/DataManage"; -import { WALLET_STORAGE_KEY_NAME } from "./config/constants"; +import { JazzIcon } from "./comp/JazzIcon"; +import { ZWalletConnect } from "./comp/ZWalletConnect"; import { DEFALUT_TOKENS } from "./config/chain_config"; +import { + NATIVE_PK_PREFIX, + TX_CONFIRM_BLOCKS, + WALLET_STORAGE_KEY_NAME, +} from "./config/constants"; +import { AllChains } from "./data/allchain"; +import { IAccount, INFT, initAccount, initNFT } from "./data/DataModel"; +import { singleton } from "./decorator/singleton.decorator"; +import { saveData } from "./manage/DataManage"; import { loadInternalWallet, restoreWalletByMnemonic, } from "./manage/WalletManage"; -import { buildLoginSignMsg, signLogin } from "./util/sign.util"; -import { JazzIcon } from "./comp/JazzIcon"; -import { ERC1155Standard } from "./standards/ERC1155Standard"; -import { ZWalletConnect } from "./comp/ZWalletConnect"; -import { getJCErc721Info, getTypeByAddress, UNKNOW } from "./util/chain.util"; -import { JCStandard } from "./standards/JCStandard"; import { NativeSvr } from "./services/NativeSvr"; import { ChainCommon } from "./standards/ChainCommon"; +import { ERC1155Standard } from "./standards/ERC1155Standard"; +import { ERC20Standard } from "./standards/ERC20Standard"; +import { ERC721Standard } from "./standards/ERC721Standard"; +import { JCStandard } from "./standards/JCStandard"; +import { + getJCErc721Info, + getTypeByAddress, + universalChainCb, + UNKNOW, +} from "./util/chain.util"; import { fromTokenMinimalUnit } from "./util/number.util"; +import { buildLoginSignMsg, signLogin } from "./util/sign.util"; var global = (typeof globalThis !== "undefined" && globalThis) || @@ -105,6 +114,7 @@ export default class JCWallet { console.log("native wallet address: " + address); var start = Date.now(); this.web3 = new Web3(this.rpcUrl); + this.web3.eth.transactionConfirmationBlocks = TX_CONFIRM_BLOCKS; console.log(`init web3 cost: ${(Date.now() - start) / 1000}`); this.erc20Standard = new ERC20Standard(this.web3); console.log("init Erc20Standard"); @@ -310,8 +320,9 @@ export default class JCWallet { // let account = this.web3.eth.accounts.create() this.wallet = this.web3.eth.accounts.wallet; const index = this.getMaxIdexOfType(0); - const nativePrefix = "0x000000000000000000000000"; - const nativePrivateKey = `${nativePrefix}${this.currentAccAddr.slice(2)}`; + const nativePrivateKey = `${NATIVE_PK_PREFIX}${this.currentAccAddr.slice( + 2 + )}`; const acc = this.web3.eth.accounts.privateKeyToAccount(nativePrivateKey); const account = this.wallet.add(acc); this.web3.eth.defaultAccount = account.address; @@ -411,12 +422,15 @@ export default class JCWallet { if (estimate) { return this.generateGasShow(gas); } - return this.web3.eth.sendTransaction({ + const reqData = { from, to, gas, value: amountToSend, - }); + tokenId: "0", + address: "eth", + }; + return universalChainCb(reqData, this.web3.eth.sendTransaction(reqData)); } public async getBalance(account?: string) { @@ -626,7 +640,7 @@ export default class JCWallet { export * from "./common/WalletEvent"; export * from "./common/ZError"; export * from "./config/chain_config"; -export * from "./util/number.util"; -export * from "./util/wallet.util"; export * from "./data/DataModel"; export * from "./lib/WalletConnect"; +export * from "./util/number.util"; +export * from "./util/wallet.util"; diff --git a/src/lib/Http.ts b/src/lib/Http.ts index 8de6477..64234b6 100644 --- a/src/lib/Http.ts +++ b/src/lib/Http.ts @@ -37,11 +37,39 @@ export async function POST(url, data) { return request(url, option); } +export async function DELETE(url, data) { + let option = { + method: "DELETE", + body: JSON.stringify(data), + }; + return request(url, option); +} + +export async function PUT(url, data) { + let option = { + method: "PUT", + body: JSON.stringify(data), + }; + return request(url, option); +} + export async function POST_JSON(url, data) { return POST(url, data).then((res) => { return res.json(); }); } + +export async function DELETE_JSON(url, data) { + return DELETE(url, data).then((res) => { + return res.json(); + }); +} + +export async function PUT_JSON(url, data) { + return PUT(url, data).then((res) => { + return res.json(); + }); +} /** * var headers = new Headers(); headers.append("Content-Type", "application/json"); diff --git a/src/queue/record.queue.ts b/src/queue/record.queue.ts new file mode 100644 index 0000000..cac2719 --- /dev/null +++ b/src/queue/record.queue.ts @@ -0,0 +1,23 @@ +import { saveRecord } from "../api/RecordApi"; +import { AsyncQueue, createAsyncQueue } from "../common/AsyncQueue"; +import { singleton } from "../decorator/singleton.decorator"; + +@singleton +export class LoggerQueue { + private queue: AsyncQueue; + + constructor() { + this.queue = createAsyncQueue(); + } + + public addLog(data: any) { + this.queue.push(async () => { + try { + await saveRecord(data); + } catch (err) { + console.log("error save tx record: "); + console.log(err); + } + }); + } +} diff --git a/src/standards/ERC20Standard.ts b/src/standards/ERC20Standard.ts index d380b90..c330628 100644 --- a/src/standards/ERC20Standard.ts +++ b/src/standards/ERC20Standard.ts @@ -166,6 +166,7 @@ export class ERC20Standard { if (estimate) { return jc.wallet.generateGasShow(gas); } + return contract.methods.transfer(to, amountBN).send({ from, gas, diff --git a/src/util/chain.util.ts b/src/util/chain.util.ts index fafef26..2cd7695 100644 --- a/src/util/chain.util.ts +++ b/src/util/chain.util.ts @@ -1,6 +1,9 @@ import { BASE_TOKEN_URI, DEFAULT_NFT_TYPES } from "../config/chain_config"; +import { TX_CONFIRM_BLOCKS } from "../config/constants"; +import { LoggerQueue } from "../queue/record.queue"; +import { toBN } from "web3-utils"; -export const UNKNOW = 'unknow'; +export const UNKNOW = "unknow"; /** * change price with customer decimals to bigNum with 18 decimals @@ -9,8 +12,8 @@ export const UNKNOW = 'unknow'; * @return {string} */ export function parsePrice(price: number, decimals: number) { - const n = 19 - decimals - return price + new Array(n).join('0') + const n = 19 - decimals; + return price + new Array(n).join("0"); } /** @@ -20,21 +23,25 @@ export function parsePrice(price: number, decimals: number) { * @param {number} fixed * @return {number | string} */ -export function formatPrice(price: number|string, decimals?: number, fixed = 2) { +export function formatPrice( + price: number | string, + decimals?: number, + fixed = 2 +) { if (!decimals) { - return price + return price; } - let str = price + '' - const length = str.length - str = str.padStart(decimals, '0') + let str = price + ""; + const length = str.length; + str = str.padStart(decimals, "0"); if (decimals >= length) { - str = '0.' + str + str = "0." + str; } else { - const pos = length - decimals - str = str.slice(0, pos) + '.' + str.slice(pos) + const pos = length - decimals; + str = str.slice(0, pos) + "." + str.slice(pos); } - str = str.slice(0, str.lastIndexOf('.') + fixed + 1) - return str + str = str.slice(0, str.lastIndexOf(".") + fixed + 1); + return str; // return str.replace(/0+$/, '').replace(/\.+$/, '') } @@ -44,19 +51,18 @@ export function formatPrice(price: number|string, decimals?: number, fixed = 2) * @return {string} */ export function toHexChainId(chainId: number) { - return '0x' + chainId.toString(16) + return "0x" + chainId.toString(16); } - export function getTypeByAddress(chain: number, address: string) { const cfgs = DEFAULT_NFT_TYPES[chain]; let categor = UNKNOW; - let type: 'erc721' | 'erc1155' = 'erc721'; + let type: "erc721" | "erc1155" = "erc721"; if (cfgs) { for (let key in cfgs) { if (cfgs[key] && cfgs[key].address === address) { - categor = key - type = cfgs[key].type + categor = key; + type = cfgs[key].type; } } } @@ -64,6 +70,55 @@ export function getTypeByAddress(chain: number, address: string) { } export async function getJCErc721Info(tokenId: string) { - const url = `${BASE_TOKEN_URI}${tokenId}` - return fetch(url).then(response => {return response.json()}) -} \ No newline at end of file + const url = `${BASE_TOKEN_URI}${tokenId}`; + return fetch(url).then((response) => { + return response.json(); + }); +} + +export function universalChainCb(reqData: any, req: any) { + return req + .on("presend", function (dataObj: any) { + if (jc.wallet.isInternal) { + console.log("before send tran: ", dataObj); + reqData.gas = toBN(dataObj.gas).toString(); + reqData.gasPrice = toBN(dataObj.gasPrice).toString(); + reqData.transactionHash = dataObj.transactionHash; + reqData.chain = jc.wallet.currentChain.id; + reqData.startTime = Date.now(); + new LoggerQueue().addLog(reqData); + } + }) + .on("transactionHash", function (hash: any) { + console.log(`remote transactionHash: ${hash}`); + }) + .on("confirmation", function (confirmationNumber, receipt) { + console.log( + `confirmation:: confirmationNumber: ${confirmationNumber} , receipt: ${receipt}` + ); + if (jc.wallet.isInternal && confirmationNumber >= TX_CONFIRM_BLOCKS) { + reqData.status = 2; + reqData.confirmTime = Date.now(); + new LoggerQueue().addLog(reqData); + } + }) + .on("receipt", function (receipt) { + console.log(receipt); + if (jc.wallet.isInternal) { + reqData.status = receipt.status ? 1 : 10; + reqData.blockNumber = receipt.blockNumber; + reqData.blockHash = receipt.blockHash; + reqData.gas = receipt.gasUsed; + new LoggerQueue().addLog(reqData); + } + return Promise.resolve(receipt); + }) + .on("error", function (error, receipt) { + console.log(error); + if (jc.wallet.isInternal) { + reqData.status = 11; + new LoggerQueue().addLog(reqData); + } + return Promise.reject(error); + }); +}