Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v4 batch #338

Merged
merged 11 commits into from
Dec 14, 2023
11 changes: 11 additions & 0 deletions .changeset/afraid-hats-occur.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/eight-pugs-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@blocto/sdk': minor
---

support web3js version 4
2 changes: 1 addition & 1 deletion packages/blocto-sdk/src/providers/blocto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class BloctoProvider implements EIP1193Provider {

// implement by children
// eslint-disable-next-line
async request(payload: RequestArguments) {}
async request(payload: RequestArguments | Array<RequestArguments>) {}

on(event: string, listener: (arg: any) => void): void {
if (!EIP1193_EVENTS.includes(event)) return;
Expand Down
184 changes: 128 additions & 56 deletions packages/blocto-sdk/src/providers/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<JsonRpcRequest>,
payload:
| JsonRpcRequest
| Array<JsonRpcRequest>
| Array<EIP1193RequestPayload>,
callback?: JsonRpcCallback
): Promise<JsonRpcResponse | Array<JsonRpcResponse> | void> {
const separateRequests = (payload: Array<JsonRpcRequest>) => {
return payload.reduce(
(
acc: {
sendRequests: JsonRpcResponse[];
otherRequests: Promise<JsonRpcResponse>[];
},
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<JsonRpcResponse>
> = 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<JsonRpcResponse>[];
},
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<JsonRpcRequest>
);

// 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(
<Array<JsonRpcResponse>>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(
<Array<JsonRpcResponse>>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(<Array<JsonRpcResponse>>originalLengthResponse);
})
.catch((error) => {
throw ethErrors.rpc.internal(error?.message);
Expand All @@ -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, <JsonRpcResponse>(<unknown>data)))
.then((data) => {
return callback(null, <JsonRpcResponse>(<unknown>data));
})
.catch((error) => callback(error));
} else {
return <JsonRpcResponse>(<unknown>handleRequest);
Expand All @@ -343,7 +403,13 @@ export default class EthereumProvider
});
}

async request(payload: EIP1193RequestPayload): Promise<any> {
async request(
payload: EIP1193RequestPayload | Array<EIP1193RequestPayload>
): Promise<any> {
// 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 } =
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -889,27 +955,33 @@ export default class EthereumProvider
): Promise<string> {
this.#checkNetworkMatched();

const extractParams = (params: Array<any>): Array<any> =>
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<any>);
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');
}
Expand Down
10 changes: 9 additions & 1 deletion packages/blocto-sdk/src/providers/types/ethereum.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export interface EthereumProviderInterface
switchableNetwork: SwitchableNetwork;
};
sendUserOperation(userOp: IUserOperation): Promise<string>;
request(args: EIP1193RequestPayload): Promise<any>;
request(
args: EIP1193RequestPayload | Array<EIP1193RequestPayload>
): Promise<any>;
loadSwitchableNetwork(
networkList: {
chainId: string;
Expand Down Expand Up @@ -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.
Expand Down
Loading