Skip to content

Commit

Permalink
Merge pull request #61 from portto/develop
Browse files Browse the repository at this point in the history
Release 0.4.1
  • Loading branch information
akira02 authored Apr 6, 2023
2 parents ddf2b03 + 639dc32 commit c27ec78
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 35 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blocto/sdk",
"version": "0.4.0",
"version": "0.4.1-beta.2",
"author": "Yang Lin",
"license": "MIT",
"repository": "[email protected]:portto/blocto-sdk.git",
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export const ETH_CHAIN_ID_SERVER_MAPPING: Mapping = {
43113: 'https://wallet-v2-dev.blocto.app',
};

export const ETH_ENV_WALLET_SERVER_MAPPING: Mapping = {
prod: 'https://wallet-v2.blocto.app',
staging: 'https://wallet-v2-staging.blocto.app',
dev: 'https://wallet-v2-dev.blocto.app',
};

/* eth series constants end */

/* sol constants begin */
Expand Down
16 changes: 16 additions & 0 deletions src/lib/getEvmSupport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface EvmSupportMapping {
[id: string | number]: {
chain_id: number
name: string // backend defined chain name: ethereum, bsc, …
display_name: string // human readable name: Ethereum, Polygon, …
network_type: string // chain network type: mainnet / testnet / devnet
blocto_service_environment: string // backend service env: prod / dev (may return staging in future)
rpc_endpoint_domains: string[] // rpc endpoints whitelist
}
}

export async function getEvmSupport(): Promise<EvmSupportMapping> {
const { networks } = await fetch('https://api.blocto.app/networks/evm').then(response => response.json());
const evmSupportMap = networks.reduce((a: any, v: any) => ({ ...a, [v.chain_id]: v }), {});
return evmSupportMap;
}
1 change: 1 addition & 0 deletions src/lib/is.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isEmail = (value: string) => /\S+@\S+\.\S+/.test(value);
156 changes: 138 additions & 18 deletions src/providers/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import invariant from 'invariant';
import { ProviderAccounts } from 'eip1193-provider';
import BloctoProvider from './blocto';
import Session from '../lib/session.d';
import { EIP1193RequestPayload, EthereumProviderConfig, EthereumProviderInterface } from './types/ethereum.d';
import {
EIP1193RequestPayload,
EthereumProviderConfig,
EthereumProviderInterface,
} from './types/ethereum.d';
import { createFrame, attachFrame, detatchFrame } from '../lib/frame';
import addSelfRemovableHandler from '../lib/addSelfRemovableHandler';
import {
Expand All @@ -16,33 +20,49 @@ import {
ETH_CHAIN_ID_CHAIN_MAPPING,
ETH_CHAIN_ID_NET_MAPPING,
ETH_CHAIN_ID_SERVER_MAPPING,
ETH_ENV_WALLET_SERVER_MAPPING,
LOGIN_PERSISTING_TIME,
DEFAULT_APP_ID,
} from '../constants';
import { KEY_SESSION } from '../lib/localStorage/constants';
import { isEmail } from '../lib/is';
import { EvmSupportMapping, getEvmSupport } from '../lib/getEvmSupport';

interface SwitchableNetwork {
[id: number | string]: {
name: string
display_name: string
network_type: string
wallet_web_url: string
rpc_url: string
}
}

function parseChainId(chainId: string | number): number {
if (typeof chainId === 'number') {
return chainId;
} else if (chainId.startsWith('0x')) {
return parseInt(chainId, 16);
}
return parseInt(chainId, 10);
}

export default class EthereumProvider extends BloctoProvider implements EthereumProviderInterface {
chainId: string | number;
networkId: string | number;
chain: string;
chainId: string | number; // current network id e.g.1
networkId: string | number; // same as chainId
chain: string; // network name "ethereum"
net: string;
rpc: string;
server: string;

accounts: Array<string> = [];
supportNetwork: EvmSupportMapping = {};
switchableNetwork: SwitchableNetwork = {};

constructor({ chainId = '0x1', rpc, server, appId }: EthereumProviderConfig) {
constructor({ chainId, rpc, server, appId }: EthereumProviderConfig) {
super();
invariant(chainId, "'chainId' is required");

if (typeof chainId === 'number') {
this.chainId = chainId;
} else if (chainId.includes('0x')) {
this.chainId = parseInt(chainId, 16);
} else {
this.chainId = parseInt(chainId, 10);
}

this.chainId = parseChainId(chainId);
this.networkId = this.chainId;
this.chain = ETH_CHAIN_ID_CHAIN_MAPPING[this.chainId];
this.net = ETH_CHAIN_ID_NET_MAPPING[this.chainId];
Expand All @@ -55,6 +75,32 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum

this.server = server || ETH_CHAIN_ID_SERVER_MAPPING[this.chainId] || process.env.SERVER || '';
this.appId = appId || process.env.APP_ID || DEFAULT_APP_ID;

this.switchableNetwork[this.chainId] = {
name: this.chain,
display_name: this.chain,
network_type: this.net,
wallet_web_url: this.server,
rpc_url: this.rpc,
};
}

private checkAndAddNetwork({ chainId, rpcUrls }:{ chainId: number; rpcUrls: string[] }): void {
const domain = new URL(rpcUrls[0]).hostname;
const { chain_id, name, display_name, network_type, blocto_service_environment, rpc_endpoint_domains } =
this.supportNetwork[chainId];
if (rpc_endpoint_domains.includes(domain)) {
const wallet_web_url = ETH_ENV_WALLET_SERVER_MAPPING[blocto_service_environment];
this.switchableNetwork[chain_id] = {
name,
display_name,
network_type,
wallet_web_url,
rpc_url: rpcUrls[0],
};
} else {
console.warn(`The rpc url ${rpcUrls[0]} is not supported.`);
}
}

private tryRetrieveSessionFromStorage(): void {
Expand All @@ -75,6 +121,27 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum
}
}

async loadSwitchableNetwork(
networkList: {
chainId: string
rpcUrls?: string[]
}[]
): Promise<null> {
return getEvmSupport().then((result) => {
// setup supported network
this.supportNetwork = result;
// setup switchable list if user set networkList
if (networkList?.length) {
networkList.forEach(({ chainId: chain_id, rpcUrls }) => {
invariant(rpcUrls, 'rpcUrls is required for networksList');
if (!rpcUrls?.length) throw new Error('Empty rpcUrls array');
this.checkAndAddNetwork({ chainId: parseChainId(chain_id), rpcUrls });
});
}
return null;
});
}

// DEPRECATED API: see https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods implementation
async send(arg1: any, arg2: any) {
switch (true) {
Expand Down Expand Up @@ -153,7 +220,12 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum
}

if (!this.connected) {
await this.enable();
const email = payload?.params?.[0];
if (payload.method === 'eth_requestAccounts' && isEmail(email)) {
await this.enable(email);
} else {
await this.enable();
}
}

try {
Expand Down Expand Up @@ -200,6 +272,49 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum
break;
case 'eth_signTransaction':
case 'eth_sendRawTransaction':
result = null;
break;
case 'wallet_addEthereumChain':
if (!payload?.params?.[0]?.chainId || !payload?.params?.[0]?.rpcUrls.length) {
throw new Error('Invalid params');
}
await getEvmSupport().then((supportNetwork) => {
this.supportNetwork = supportNetwork;
this.checkAndAddNetwork({
chainId: parseChainId(payload?.params?.[0]?.chainId),
rpcUrls: payload?.params?.[0].rpcUrls,
});
});
result = null;
break;
case 'wallet_switchEthereumChain':
if (!payload?.params?.[0]?.chainId) {
throw new Error('Invalid params');
}

if (!this.switchableNetwork[parseChainId(payload.params[0].chainId)]) {
const error: any = new Error(
'This chain has not been added to SDK. Please try wallet_addEthereumChain first.'
);
// Follows MetaMask return 4902, see https://docs.metamask.io/guide/rpc-api.html#wallet-switchethereumchain
error.code = 4902;
throw error;
}

this.chainId = parseChainId(payload.params[0].chainId);
this.networkId = this.chainId;
this.chain = this.switchableNetwork[this.chainId].name;
this.net = this.switchableNetwork[this.chainId].network_type;

invariant(this.chain, `unsupported 'chainId': ${this.chainId}`);

this.rpc = this.switchableNetwork[this.chainId].rpc_url;

invariant(this.rpc, "'rpc' is required");

this.server = this.switchableNetwork[this.chainId].wallet_web_url;
this.accounts = await this.fetchAccounts();

result = null;
break;
default:
Expand All @@ -222,7 +337,7 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum

// eip-1102 alias
// DEPRECATED API: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1102.md
async enable(): Promise<ProviderAccounts> {
async enable(email?: string): Promise<ProviderAccounts> {
const existedSDK = (window as any).ethereum;
if (existedSDK && existedSDK.isBlocto) {
if (parseInt(existedSDK.chainId, 16) !== this.chainId) {
Expand Down Expand Up @@ -251,8 +366,13 @@ export default class EthereumProvider extends BloctoProvider implements Ethereum
return resolve(this.accounts);
}

const location = encodeURIComponent(window.location.origin);
const loginFrame = createFrame(`${this.server}/${this.appId}/${this.chain}/authn?l6n=${location}`);
const params = new URLSearchParams();
params.set('l6n', window.location.origin);
const emailParam = email && isEmail(email) ? `/${email}` : '';

const loginFrame = createFrame(
`${this.server}/${this.appId}/${this.chain}/authn${emailParam}?${params.toString()}`
);

attachFrame(loginFrame);

Expand Down
40 changes: 25 additions & 15 deletions src/providers/types/ethereum.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,36 @@ import { BaseConfig } from '../../constants';
import BloctoProviderInterface from './blocto.d';

export declare interface EthereumProviderConfig extends BaseConfig {
chainId: string | number | null;
rpc?: string;
server?: string;
chainId: string | number | null
rpc?: string
server?: string
}

export interface EIP1193RequestPayload {
id?: number;
jsonrpc?: string;
method: string;
params?: Array<any>;
id?: number
jsonrpc?: string
method: string
params?: Array<any>
}

export declare interface EthereumProviderInterface extends BloctoProviderInterface, IEthereumProvider {
chainId: string | number;
networkId: string | number;
chain: string;
net: string;
rpc: string;
server: string;
accounts: Array<string>;
request(args: EIP1193RequestPayload): Promise<any>;
chainId: string | number
networkId: string | number
chain: string
net: string
rpc: string
server: string
accounts: Array<string>
request(args: EIP1193RequestPayload): Promise<any>
loadSwitchableNetwork(
networkList: {
chainId: string
rpcUrls?: string[]
}[]
): Promise<null>
}

export interface AddEthereumChainParameter {
chainId: string
rpcUrls: string[]
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
Expand Down

0 comments on commit c27ec78

Please sign in to comment.