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: add frame connector #886

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/frame/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @web3-react/frame
30 changes: 30 additions & 0 deletions packages/frame/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@web3-react/frame",
"keywords": [
"web3-react",
"frame"
],
"author": "Jean-Benoît RICHEZ <[email protected]>",
"license": "GPL-3.0-or-later",
"repository": "github:Uniswap/web3-react",
"publishConfig": {
"access": "public"
},
"version": "8.0.7-beta.0",
"files": [
"dist/*"
],
"type": "commonjs",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"scripts": {
"prebuild": "rm -rf dist",
"build": "tsc",
"start": "tsc --watch"
},
"dependencies": {
"eth-provider": "^0.13.6",
"@web3-react/types": "^8.0.20-beta.0"
}
}
164 changes: 164 additions & 0 deletions packages/frame/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Actions, AddEthereumChainParameter, Connector, Provider, ProviderConnectInfo, ProviderRpcError, WatchAssetParameters } from "@web3-react/types";
import ethProvider from "eth-provider";

function parseChainId(chainId: string) {
return Number.parseInt(chainId, 16)
}

export class NoFrameError extends Error {
public constructor() {
super('Frame not installed')
this.name = NoFrameError.name
Object.setPrototypeOf(this, NoFrameError.prototype)
}
}

export type FrameProvider = Provider & {
isConnected?: () => boolean
}

export interface FrameConstructorArgs{
actions: Actions
// options?: Parameters<typeof detectEthereumProvider>[0]
onError?: (error: Error) => void
}

export class Frame extends Connector{

/** {@inheritdoc Connector.provider} */
declare public provider?: FrameProvider;
private eagerConnection?: Promise<void>;

constructor({ actions, onError }: FrameConstructorArgs) {
super(actions, onError)
}

private async isomorphicInitialize(): Promise<void> {
if (this.eagerConnection) return;

const provider = ethProvider('frame');
if(!provider) return;

this.provider = provider as FrameProvider;

if(this.provider){
this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) })
});

this.provider.on('disconnect', (error: ProviderRpcError): void => {
this.actions.resetState()
this.onError?.(error)
});

this.provider.on('chainChanged', (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) })
});

this.provider.on('accountsChanged', (accounts: string[]): void => {
if (accounts.length === 0) {
// handle this edge case by disconnecting
this.actions.resetState()
} else {
this.actions.update({ accounts })
}
});
}

}

/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation()

try {
await this.isomorphicInitialize()
if (!this.provider) return cancelActivation()

// Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing
// chains; they should be requested serially, with accounts first, so that the chainId can settle.
const accounts = (await this.provider.request({ method: 'eth_accounts' })) as string[];

if (!accounts.length) throw new Error('No accounts returned');

const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string
this.actions.update({ chainId: parseChainId(chainId), accounts })
} catch (error) {
console.debug('Could not connect eagerly', error)
this.actions.resetState()
}
}

/**
* Initiates a connection.
*
* @param desiredChainIdOrChainParameters - If defined, indicates the desired chain to connect to. If the user is
* already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch
* to the chain, if one of two conditions is met: either they already have it added in their extension, or the
* argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the
* specified parameters first, before being prompted to switch.
*/
public async activate(desiredChainIdOrChainParameters?: number | AddEthereumChainParameter): Promise<void> {
let cancelActivation: () => void
if (!this.provider?.isConnected?.()) cancelActivation = this.actions.startActivation()

return this.isomorphicInitialize()
.then(async () => {
if (!this.provider) throw new NoFrameError()

// Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing
// chains; they should be requested serially, with accounts first, so that the chainId can settle.
const accounts = (await this.provider.request({ method: 'eth_requestAccounts' })) as string[]
const chainId = (await this.provider.request({ method: 'eth_chainId' })) as string
const receivedChainId = parseChainId(chainId)
const desiredChainId =
typeof desiredChainIdOrChainParameters === 'number'
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId

// if there's no desired chain, or it's equal to the received, update
if (!desiredChainId || receivedChainId === desiredChainId)
return this.actions.update({ chainId: receivedChainId, accounts })

const desiredChainIdHex = `0x${desiredChainId.toString(16)}`

// if we're here, we can try to switch networks
return this.provider
.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: desiredChainIdHex }],
})
.catch((error: ProviderRpcError) => {
throw error
})
.then(() => this.activate(desiredChainId))
})
.catch((error) => {
cancelActivation?.()
throw error
})
}

public async watchAsset({ address, symbol, decimals, image }: WatchAssetParameters): Promise<true> {
if (!this.provider) throw new Error('No provider')

return this.provider
.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: {
address, // The address that the token is at.
symbol, // A ticker symbol or shorthand, up to 5 chars.
decimals, // The number of decimals in the token
image, // A string url of the token logo
},
},
})
.then((success) => {
if (!success) throw new Error('Rejected')
return true
})
}

}
8 changes: 8 additions & 0 deletions packages/frame/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"outDir": "./dist"
}
}

80 changes: 80 additions & 0 deletions packages/frame/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@web3-react/types@^8.0.20-beta.0":
version "8.2.2"
resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.2.2.tgz#1ae7f11069d9a9c711aa4152f95331747fb1e551"
integrity sha512-PrPrJNjJhUX3lL/365llAZwY0bpUm9N52OjGMFyzCIX7IR13f7WLUk/LyQa9ALneCBu3cJUVTZANuFdqdREuvw==
dependencies:
zustand "4.4.0"

cookiejar@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==

eth-provider@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/eth-provider/-/eth-provider-0.13.6.tgz#664ad8a5b0aa5db41ff419e6cc1081b4588f1c12"
integrity sha512-/i0qSQby/rt3CCZrNVlgBdCUYQBwULStFRlBt7+ULNVpwbsYWl9VWXFaQxsbJLOo0x7swRS3OknIdlxlunsGJw==
dependencies:
ethereum-provider "0.7.7"
events "3.3.0"
oboe "2.1.5"
uuid "9.0.0"
ws "8.9.0"
xhr2-cookies "1.1.0"

[email protected]:
version "0.7.7"
resolved "https://registry.yarnpkg.com/ethereum-provider/-/ethereum-provider-0.7.7.tgz#c67c69aa9ced8f728dacc2b4c00ad4a8bf329319"
integrity sha512-ulbjKgu1p2IqtZqNTNfzXysvFJrMR3oTmWEEX3DnoEae7WLd4MkY4u82kvXhxA2C171rK8IVlcodENX7TXvHTA==
dependencies:
events "3.3.0"

[email protected]:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==

http-https@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b"
integrity sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg==

[email protected]:
version "2.1.5"
resolved "https://registry.yarnpkg.com/oboe/-/oboe-2.1.5.tgz#5554284c543a2266d7a38f17e073821fbde393cd"
integrity sha512-zRFWiF+FoicxEs3jNI/WYUrVEgA7DeET/InK0XQuudGHRg8iIob3cNPrJTKaz4004uaA9Pbe+Dwa8iluhjLZWA==
dependencies:
http-https "^1.0.0"

[email protected]:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==

[email protected]:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==

[email protected]:
version "8.9.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"
integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==

[email protected]:
version "1.1.0"
resolved "https://registry.yarnpkg.com/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz#7d77449d0999197f155cb73b23df72505ed89d48"
integrity sha512-hjXUA6q+jl/bd8ADHcVfFsSPIf+tyLIjuO9TwJC9WI6JP2zKcS7C+p56I9kCLLsaCiNT035iYvEUUzdEFj/8+g==
dependencies:
cookiejar "^2.1.1"

[email protected]:
version "4.4.0"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.0.tgz#13b3e8ca959dd53d536034440aec382ff91b65c3"
integrity sha512-2dq6wq4dSxbiPTamGar0NlIG/av0wpyWZJGeQYtUOLegIUvhM2Bf86ekPlmgpUtS5uR7HyetSiktYrGsdsyZgQ==
dependencies:
use-sync-external-store "1.2.0"