diff --git a/.changeset/afraid-hats-occur.md b/.changeset/afraid-hats-occur.md new file mode 100644 index 00000000..6ccfc5d6 --- /dev/null +++ b/.changeset/afraid-hats-occur.md @@ -0,0 +1,11 @@ +--- +'@blocto/aptos-wallet-adapter-plugin': patch +'@blocto/connectkit-connector': patch +'@blocto/rainbowkit-connector': patch +'@blocto/web3-react-connector': patch +'@blocto/web3modal-connector': patch +'@blocto/wagmi-connector': patch +'@blocto/sdk': patch +--- + +support web3js v4 diff --git a/.changeset/eight-pugs-shop.md b/.changeset/eight-pugs-shop.md new file mode 100644 index 00000000..0db00490 --- /dev/null +++ b/.changeset/eight-pugs-shop.md @@ -0,0 +1,5 @@ +--- +'@blocto/sdk': minor +--- + +support web3js version 4 diff --git a/packages/blocto-sdk/src/providers/blocto.ts b/packages/blocto-sdk/src/providers/blocto.ts index e8aba78b..a86d9c5b 100644 --- a/packages/blocto-sdk/src/providers/blocto.ts +++ b/packages/blocto-sdk/src/providers/blocto.ts @@ -23,7 +23,7 @@ class BloctoProvider implements EIP1193Provider { // implement by children // eslint-disable-next-line - async request(payload: RequestArguments) {} + async request(payload: RequestArguments | Array) {} on(event: string, listener: (arg: any) => void): void { if (!EIP1193_EVENTS.includes(event)) return; diff --git a/packages/blocto-sdk/src/providers/ethereum.ts b/packages/blocto-sdk/src/providers/ethereum.ts index c355d6c8..29922051 100644 --- a/packages/blocto-sdk/src/providers/ethereum.ts +++ b/packages/blocto-sdk/src/providers/ethereum.ts @@ -11,6 +11,7 @@ import { JsonRpcCallback, SwitchableNetwork, IUserOperation, + PromiseResponseItem, } from './types/ethereum.d'; import { createFrame, attachFrame, detatchFrame } from '../lib/frame'; import addSelfRemovableHandler from '../lib/addSelfRemovableHandler'; @@ -253,63 +254,120 @@ export default class EthereumProvider // DEPRECATED API: see https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods implementation // web3 v1.x BatchRequest still depends on it so we need to implement anyway ¯\_(ツ)_/¯ async sendAsync( - payload: JsonRpcRequest | Array, + payload: + | JsonRpcRequest + | Array + | Array, callback?: JsonRpcCallback ): Promise | void> { + const separateRequests = (payload: Array) => { + return payload.reduce( + ( + acc: { + sendRequests: JsonRpcResponse[]; + otherRequests: Promise[]; + }, + request: JsonRpcRequest + ) => { + if (request.method === 'eth_sendTransaction') { + acc.sendRequests.push(request.params?.[0]); + } else { + acc.otherRequests.push( + this.request(request as EIP1193RequestPayload) + ); + } + return acc; + }, + { sendRequests: [], otherRequests: [] } + ); + }; + + function createBaseResponse(item: JsonRpcResponse): JsonRpcResponse { + return { + id: String(item.id), + jsonrpc: '2.0', + method: item.method, + }; + } + + function processResponses( + payload: JsonRpcRequest[], + responses: PromiseResponseItem[] + ): JsonRpcResponse[] { + const processedResponses: JsonRpcResponse[] = []; + let responseIndex = 1; + payload.forEach((item) => { + const baseResponse = createBaseResponse(item as JsonRpcResponse); + if (item.method === 'eth_sendTransaction') { + baseResponse.result = responses[0].value; + baseResponse.error = + responses[0].status !== 'fulfilled' + ? responses[0].reason + : undefined; + } else { + if (responseIndex < responses.length) { + baseResponse.result = responses[responseIndex].value; + baseResponse.error = + responses[responseIndex].status !== 'fulfilled' + ? responses[responseIndex].reason + : undefined; + responseIndex++; + } + } + processedResponses.push(baseResponse); + }); + + return processedResponses; + } + const handleRequest: Promise< void | JsonRpcResponse | Array > = new Promise((resolve) => { // web3 v1.x concat batched JSON-RPC requests to an array, handle it here if (Array.isArray(payload)) { - const { sendRequests, otherRequests } = payload.reduce( - ( - acc: { - sendRequests: JsonRpcResponse[]; - otherRequests: Promise[]; - }, - request: JsonRpcRequest - ) => { - if (request.method === 'eth_sendTransaction') { - acc.sendRequests.push(request.params?.[0]); - } else { - acc.otherRequests.push( - this.request(request as EIP1193RequestPayload) - ); - } - return acc; - }, - { sendRequests: [], otherRequests: [] } + const { sendRequests, otherRequests } = separateRequests( + payload as Array ); // collect transactions and send batch with custom method const batchReqPayload = { - method: 'blocto_sendBatchTransaction', - params: sendRequests, + method: 'wallet_sendMultiCallTransaction', + params: [sendRequests, false], }; - const batchReqPromise = this.request(batchReqPayload); - + const isSendRequestsEmpty = sendRequests.length === 0; const idBase = Math.floor(Math.random() * 10000); + const allPromise = isSendRequestsEmpty + ? [...otherRequests] + : [this.request(batchReqPayload), ...otherRequests]; // resolve response when all request are executed - Promise.allSettled([batchReqPromise, ...otherRequests]) + Promise.allSettled(allPromise) .then((responses) => { - return resolve( - >responses.map((response, index) => { - return { - id: String(payload[index].id || idBase + index + 1), - jsonrpc: '2.0', - method: payload[index].method, - result: - response.status === 'fulfilled' - ? response.value - : undefined, - error: - response.status !== 'fulfilled' - ? response.reason - : undefined, - }; - }) + if (isSendRequestsEmpty) { + return resolve( + >responses.map((response, index) => { + return { + id: String(payload[index]?.id || idBase + index + 1), + jsonrpc: '2.0', + method: payload[index].method, + result: + response.status === 'fulfilled' + ? response.value + : undefined, + error: + response.status !== 'fulfilled' + ? response.reason + : undefined, + }; + }) + ); + } + const originalLengthResponse = processResponses( + payload as JsonRpcRequest[], + responses ); + + return resolve(>originalLengthResponse); }) .catch((error) => { throw ethErrors.rpc.internal(error?.message); @@ -322,7 +380,9 @@ export default class EthereumProvider // execute callback or return promise, depdends on callback arg given or not if (typeof callback === 'function') { handleRequest - .then((data) => callback(null, (data))) + .then((data) => { + return callback(null, (data)); + }) .catch((error) => callback(error)); } else { return (handleRequest); @@ -343,7 +403,13 @@ export default class EthereumProvider }); } - async request(payload: EIP1193RequestPayload): Promise { + async request( + payload: EIP1193RequestPayload | Array + ): Promise { + // web3.js v4 batch entry point + if (Array.isArray(payload)) { + return this.sendAsync(payload); + } if (!payload?.method) throw ethErrors.rpc.invalidRequest(); const { blockchainName, switchableNetwork, sessionKeyEnv } = @@ -437,7 +503,7 @@ export default class EthereumProvider case 'eth_sendTransaction': result = await this.handleSendTransaction(payload); break; - case 'blocto_sendBatchTransaction': + case 'wallet_sendMultiCallTransaction': result = await this.handleSendBatchTransaction(payload); break; case 'eth_signTransaction': @@ -889,27 +955,33 @@ export default class EthereumProvider ): Promise { this.#checkNetworkMatched(); - const extractParams = (params: Array): Array => - params.map((param) => - 'params' in param - ? param.params[0] // handle passing web3.eth.sendTransaction.request(...) as a parameter with params - : param - ); - const formatParams = extractParams(payload.params as Array); - const copyPayload = { ...payload, params: formatParams }; + let originalParams, revertFlag; + if (Array.isArray(payload.params) && payload.params.length >= 2) { + [originalParams, revertFlag] = payload.params; + } else { + originalParams = payload.params; + revertFlag = false; + } - const { isValid, invalidMsg } = isValidTransactions(copyPayload.params); + const revert = revertFlag ? revertFlag : false; + + const { isValid, invalidMsg } = isValidTransactions(originalParams); if (!isValid) { throw ethErrors.rpc.invalidParams(invalidMsg); } - - return this.#createAuthzFrame(copyPayload.params); + return this.#createAuthzFrame(originalParams, revert); } - async #createAuthzFrame(params: EIP1193RequestPayload['params']) { + async #createAuthzFrame( + params: EIP1193RequestPayload['params'], + revert = true + ) { const { authorizationId } = await this.bloctoApi<{ authorizationId: string; - }>(`/authz`, { method: 'POST', body: JSON.stringify(params) }); + }>(`/authz`, { + method: 'POST', + body: JSON.stringify([params, revert]), + }); const authzFrame = await this.setIframe(`/authz/${authorizationId}`); return this.responseListener(authzFrame, 'txHash'); } diff --git a/packages/blocto-sdk/src/providers/types/ethereum.d.ts b/packages/blocto-sdk/src/providers/types/ethereum.d.ts index 9ce5bc07..f04fcbff 100644 --- a/packages/blocto-sdk/src/providers/types/ethereum.d.ts +++ b/packages/blocto-sdk/src/providers/types/ethereum.d.ts @@ -48,7 +48,9 @@ export interface EthereumProviderInterface switchableNetwork: SwitchableNetwork; }; sendUserOperation(userOp: IUserOperation): Promise; - request(args: EIP1193RequestPayload): Promise; + request( + args: EIP1193RequestPayload | Array + ): Promise; loadSwitchableNetwork( networkList: { chainId: string; @@ -85,6 +87,12 @@ export type JsonRpcCallback = ( response?: JsonRpcResponse ) => unknown; +export interface PromiseResponseItem { + status: 'fulfilled' | 'rejected'; + value?: any; + reason?: any; +} + /** * A [[HexString]] whose length is even, which ensures it is a valid * representation of binary data.