mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-24 22:30:35 +08:00
Add WalletConnect support
Co-authored-by: Njoku Emmanuel <kalunjoku123@gmail.com> Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
co-authored by
Njoku Emmanuel
parent
767355b782
commit
400a2df0a4
Generated
+1693
-20
File diff suppressed because it is too large
Load Diff
@@ -31,12 +31,20 @@
|
||||
"@gnosis.pm/safe-service-client": "^0.1.0-alpha.0",
|
||||
"@stardazed/streams": "^3.1.0",
|
||||
"@types/marked": "^2.0.3",
|
||||
"@ethersproject/abstract-provider": "^5.4.0",
|
||||
"@walletconnect/client": "^1.4.2-rc.2",
|
||||
"@walletconnect/signer-connection": "^1.4.2-rc.2",
|
||||
"@walletconnect/types": "^1.4.2-rc.2",
|
||||
"@walletconnect/utils": "^1.4.2-rc.2",
|
||||
"buffer": "^6.0.3",
|
||||
"ethereum-blockies": "^0.1.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"eip1193-provider": "1.0.1",
|
||||
"ethers": "^5.0.31",
|
||||
"marked": "^2.0.7",
|
||||
"multibase": "^4.0.4",
|
||||
"multihashes": "^4.0.2",
|
||||
"pure-svg-code": "^1.0.6",
|
||||
"svelte": "^3.32.3",
|
||||
"svelte-preprocess": "^4.7.2",
|
||||
"svelte-routing": "^1.6.0"
|
||||
|
||||
+7
-1
@@ -15,9 +15,15 @@
|
||||
import Loading from '@app/Loading.svelte';
|
||||
import Modal from '@app/Modal.svelte';
|
||||
|
||||
const loadConfig = getConfig().then(cfg => {
|
||||
const loadConfig = getConfig().then(async cfg => {
|
||||
if ($state.connection === Connection.Connected) {
|
||||
state.refreshBalance(cfg);
|
||||
} else if ($state.connection === Connection.Disconnected) {
|
||||
// Update the session state if we're already connected to WalletConnect
|
||||
// from a previous session.
|
||||
if (cfg.walletConnect.client.connected) {
|
||||
await state.connectWalletConnect(cfg);
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
});
|
||||
|
||||
+34
-7
@@ -1,23 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Connection } from "@app/session";
|
||||
import { state } from '@app/session';
|
||||
import { get } from "svelte/store";
|
||||
import { Connection, state } from "@app/session";
|
||||
import type { Err } from '@app/error';
|
||||
import Error from '@app/Error.svelte';
|
||||
import type { Config } from '@app/config';
|
||||
import ConnectWallet from "@app/components/Modal/ConnectWallet.svelte";
|
||||
|
||||
export let config: Config;
|
||||
export let caption = "Connect";
|
||||
export let className = "";
|
||||
export let style = "";
|
||||
export let config: Config;
|
||||
|
||||
let walletUnavailable = !window.ethereum;
|
||||
let error: Err | null = null;
|
||||
|
||||
const onModalClose = () => {
|
||||
const wcs = get(config.walletConnect.state);
|
||||
|
||||
if (wcs.state === "open") {
|
||||
config.walletConnect.state.set({ state: "close" });
|
||||
wcs.onClose();
|
||||
}
|
||||
};
|
||||
const onConnect = async () => {
|
||||
try {
|
||||
await state.connectWalletConnect(config);
|
||||
} catch (e) {
|
||||
walletConnectState.set({ state: "close" });
|
||||
error = e;
|
||||
}
|
||||
};
|
||||
|
||||
$: connecting = $state.connection === Connection.Connecting;
|
||||
$: walletConnectState = config.walletConnect.state;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<button
|
||||
on:click={() => state.connect(config)}
|
||||
on:click|stopPropagation={onConnect}
|
||||
{style}
|
||||
class="connect {className}"
|
||||
disabled={connecting || walletUnavailable}
|
||||
@@ -29,3 +48,11 @@
|
||||
{caption}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if $walletConnectState.state === "open"}
|
||||
<ConnectWallet {config} uri={$walletConnectState.uri} on:close={onModalClose} />
|
||||
{:else if error}
|
||||
<Error floating title="Wallet connection failed" {error}
|
||||
on:close={() => error = null} />
|
||||
{/if}
|
||||
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@
|
||||
</span>
|
||||
|
||||
<button class="address outline small" bind:this={sessionButton}
|
||||
on:click={disconnectWallet}
|
||||
on:click={() => disconnectWallet(config)}
|
||||
on:mouseover={() => sessionButtonHover = true}
|
||||
on:mouseout={() => sessionButtonHover = false}
|
||||
>
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@
|
||||
export let subtle = false;
|
||||
export let small = false;
|
||||
export let narrow = false;
|
||||
export let center = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -96,7 +97,7 @@
|
||||
<div class="modal-overlay"></div>
|
||||
{/if}
|
||||
|
||||
<div class:modal-floating={floating} class="centered">
|
||||
<div class:modal-floating={floating} class:centered={!center}>
|
||||
<div class="modal" class:error
|
||||
class:modal-subtle={subtle}
|
||||
class:modal-narrow={narrow}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import type WalletConnect from "@walletconnect/client";
|
||||
import * as ethers from "ethers";
|
||||
import * as ethersBytes from "@ethersproject/bytes";
|
||||
import type { TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
|
||||
import { Deferrable, resolveProperties } from "@ethersproject/properties";
|
||||
import { _TypedDataEncoder } from "ethers/lib/utils";
|
||||
import type { TypedDataDomain, TypedDataField } from "@ethersproject/abstract-signer";
|
||||
|
||||
export class WalletConnectSigner extends ethers.Signer {
|
||||
public walletConnect: WalletConnect;
|
||||
public readonly provider: ethers.providers.JsonRpcProvider;
|
||||
|
||||
constructor(
|
||||
walletConnect: WalletConnect,
|
||||
provider: ethers.providers.JsonRpcProvider,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.provider = provider;
|
||||
this.walletConnect = walletConnect;
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
const accountAddress = this.walletConnect.accounts[0];
|
||||
if (!accountAddress) {
|
||||
throw new Error(
|
||||
"The connected wallet has no accounts or there is a connection problem"
|
||||
);
|
||||
}
|
||||
return accountAddress;
|
||||
}
|
||||
|
||||
async _signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
|
||||
// Populate any ENS names (in-place)
|
||||
const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => {
|
||||
return this.provider.resolveName(name);
|
||||
});
|
||||
|
||||
const address = await this.getAddress();
|
||||
const signature = await this.walletConnect.signTypedData([
|
||||
address.toLowerCase(),
|
||||
JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)),
|
||||
]);
|
||||
return signature;
|
||||
}
|
||||
|
||||
async signMessage(message: ethers.Bytes | string): Promise<string> {
|
||||
const prefix = ethers.utils.toUtf8Bytes(
|
||||
`\x19Ethereum Signed Message:\n${message.length}`
|
||||
);
|
||||
const msg = ethers.utils.concat([prefix, message]);
|
||||
const address = await this.getAddress();
|
||||
const keccakMessage = ethers.utils.keccak256(msg);
|
||||
const signature = await this.walletConnect.signMessage([
|
||||
address.toLowerCase(),
|
||||
keccakMessage,
|
||||
]);
|
||||
return signature;
|
||||
}
|
||||
|
||||
async sendTransaction(
|
||||
transaction: Deferrable<TransactionRequest>
|
||||
): Promise<TransactionResponse> {
|
||||
const tx = await resolveProperties(transaction);
|
||||
const from = tx.from || (await this.getAddress());
|
||||
|
||||
const txHash = await this.walletConnect.sendTransaction({
|
||||
from,
|
||||
to: tx.to,
|
||||
value: maybeBigNumberToString(tx.value),
|
||||
data: bytesLikeToString(tx.data),
|
||||
});
|
||||
return <TransactionResponse>{
|
||||
hash: txHash,
|
||||
nonce: tx.nonce,
|
||||
gasLimit: tx.gasLimit,
|
||||
gasPrice: tx.gasPrice,
|
||||
data: bytesLikeToString(tx.data) || "",
|
||||
value: tx.value,
|
||||
chainId: tx.chainId,
|
||||
confirmations: 0,
|
||||
from: from,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider?.waitForTransaction(txHash, confirmations);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async signTransaction(
|
||||
transaction: Deferrable<TransactionRequest>
|
||||
): Promise<string> {
|
||||
const tx = await resolveProperties(transaction);
|
||||
const from = tx.from || (await this.getAddress());
|
||||
const nonce = await this.provider.getTransactionCount(from);
|
||||
|
||||
const signedTx = await this.walletConnect.signTransaction({
|
||||
from,
|
||||
to: tx.to,
|
||||
value: maybeBigNumberToString(tx.value || 0),
|
||||
gasLimit: maybeBigNumberToString(tx.gasLimit || 200 * 1000),
|
||||
gasPrice: maybeBigNumberToString(tx.gasPrice || 0),
|
||||
nonce,
|
||||
data: bytesLikeToString(tx.data),
|
||||
});
|
||||
return signedTx;
|
||||
}
|
||||
|
||||
connect(): ethers.Signer {
|
||||
throw new Error("WalletConnectSigner.connect should never be called");
|
||||
}
|
||||
}
|
||||
|
||||
function maybeBigNumberToString(
|
||||
bn: ethers.BigNumberish | undefined
|
||||
): string | undefined {
|
||||
if (bn === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return ethers.BigNumber.from(bn).toString();
|
||||
}
|
||||
}
|
||||
|
||||
function bytesLikeToString(
|
||||
bytes: ethersBytes.BytesLike | undefined
|
||||
): string | undefined {
|
||||
if (bytes === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return ethersBytes.hexlify(bytes);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// TODO: Show "look at your wallet" / "confirm tx" before state change.
|
||||
import { ethers } from 'ethers';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { BigNumber } from 'ethers';
|
||||
@@ -99,7 +98,7 @@ export async function registrationFee(config: Config): Promise<BigNumber> {
|
||||
}
|
||||
|
||||
export async function registerName(name: string, owner: string, config: Config): Promise<void> {
|
||||
assert(config.signer);
|
||||
assert(config.signer, "signer is not available");
|
||||
|
||||
if (! name) return;
|
||||
|
||||
@@ -144,7 +143,7 @@ async function commitAndRegister(name: string, owner: string, config: Config): P
|
||||
}
|
||||
|
||||
async function commit(commitment: string, fee: BigNumber, minAge: number, config: Config): Promise<void> {
|
||||
assert(config.signer);
|
||||
assert(config.signer, "signer is not available");
|
||||
|
||||
const owner = config.signer;
|
||||
const ownerAddr = await owner.getAddress();
|
||||
@@ -187,8 +186,7 @@ async function permitSignature(
|
||||
value: ethers.BigNumberish,
|
||||
deadline: ethers.BigNumberish,
|
||||
): Promise<ethers.Signature> {
|
||||
assert(owner.provider);
|
||||
|
||||
assert(owner.provider, "provider is not available");
|
||||
state.set({ connection: State.SigningPermit });
|
||||
|
||||
const ownerAddr = await owner.getAddress();
|
||||
@@ -222,7 +220,7 @@ async function permitSignature(
|
||||
}
|
||||
|
||||
async function register(name: string, owner: string, salt: Uint8Array, config: Config) {
|
||||
assert(config.signer);
|
||||
assert(config.signer, "signer is not available");
|
||||
state.set({ connection: State.SigningRegister });
|
||||
|
||||
const tx = await registrar(config).connect(config.signer).register(
|
||||
@@ -233,7 +231,7 @@ async function register(name: string, owner: string, salt: Uint8Array, config: C
|
||||
console.log("Sent", tx);
|
||||
|
||||
await tx.wait();
|
||||
window.localStorage.clear();
|
||||
window.localStorage.removeItem("commitment");
|
||||
state.set({ connection: State.Registered });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { qrcode } from "pure-svg-code";
|
||||
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import { state } from "@app/session";
|
||||
import type { Config } from "@app/config";
|
||||
|
||||
export let uri: string;
|
||||
export let config: Config;
|
||||
|
||||
$: svgString = qrcode({
|
||||
content: uri,
|
||||
width: 200,
|
||||
height: 200,
|
||||
color: "black",
|
||||
padding: 0,
|
||||
background: "white",
|
||||
ecl: "M",
|
||||
});
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const onClickConnect = () => { state.connectMetamask(config); };
|
||||
const onClose = () => { dispatch("close"); };
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.qrcode-wrapper {
|
||||
width: min-content;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
}
|
||||
.qrcode {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.wrapper {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
<Modal floating center>
|
||||
<div slot="title">
|
||||
<div>👛</div>
|
||||
<div>Connect your wallet</div>
|
||||
</div>
|
||||
|
||||
<div slot="subtitle">
|
||||
<div class="text-small">
|
||||
WalletConnect<br/>
|
||||
<a href="https://walletconnect.org/wallets" class="link">
|
||||
View compatible wallets
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="body">
|
||||
<div class="qrcode-wrapper">
|
||||
<div class="qrcode">
|
||||
{@html svgString}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="actions">
|
||||
<button class="secondary tiny text-small" on:click={onClickConnect} disabled={!config.metamaskSigner}>
|
||||
Connect with Metamask
|
||||
</button>
|
||||
<button class="text tiny text-small" on:click={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
@@ -48,6 +48,7 @@
|
||||
"tokens": []
|
||||
},
|
||||
"alchemy": { "key": "cQFlLK8EokIGlJhd_soImwEyUoC7Ec8r" },
|
||||
"walletConnect": { "bridge": "https://radicle.bridge.walletconnect.org" },
|
||||
"ceramic": { "api": "https://ceramic-clay.3boxlabs.com" },
|
||||
"radicle": { "api": "http://0.0.0.0:8888" },
|
||||
"ipfs": { "gateway": "https://ipfs.io/ipfs/" },
|
||||
|
||||
+140
-36
@@ -1,9 +1,13 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { ethers } from "ethers";
|
||||
import type { TypedDataSigner } from '@ethersproject/abstract-signer';
|
||||
import SafeServiceClient from "@gnosis.pm/safe-service-client";
|
||||
import CeramicClient from "@ceramicnetwork/http-client";
|
||||
import { IDX } from "@ceramicstudio/idx";
|
||||
import WalletConnect from "@walletconnect/client";
|
||||
import config from "@app/config.json";
|
||||
import { WalletConnectSigner } from "./WalletConnectSigner";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -12,6 +16,15 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gas limits for various transactions.
|
||||
const gasLimits = {
|
||||
createOrg: 1_200_000,
|
||||
};
|
||||
|
||||
export type WalletConnectState =
|
||||
{ state: "close" }
|
||||
| { state: "open"; uri: string; onClose: any };
|
||||
|
||||
export class Config {
|
||||
network: { name: string; chainId: number };
|
||||
registrar: { address: string; domain: string };
|
||||
@@ -21,7 +34,14 @@ export class Config {
|
||||
orgs: { subgraph: string; contractHash: string };
|
||||
gasLimits: { createOrg: number };
|
||||
provider: ethers.providers.JsonRpcProvider;
|
||||
signer: ethers.Signer & TypedDataSigner | null;
|
||||
signer: ethers.Signer & TypedDataSigner | WalletConnectSigner | null;
|
||||
walletConnect: {
|
||||
client: WalletConnect;
|
||||
bridge: string;
|
||||
signer: WalletConnectSigner;
|
||||
state: Writable<WalletConnectState>;
|
||||
};
|
||||
metamaskSigner: ethers.Signer & TypedDataSigner | null;
|
||||
safe: {
|
||||
api?: string;
|
||||
client?: SafeServiceClient;
|
||||
@@ -36,29 +56,31 @@ export class Config {
|
||||
|
||||
constructor(
|
||||
network: { name: string; chainId: number },
|
||||
provider: ethers.providers.JsonRpcProvider | null,
|
||||
signer: ethers.Signer & TypedDataSigner | null,
|
||||
provider: ethers.providers.JsonRpcProvider,
|
||||
metamaskSigner: ethers.Signer & TypedDataSigner | null,
|
||||
) {
|
||||
const cfg = (<Record<string, any>> config)[network.name];
|
||||
const api = config.radicle.api;
|
||||
|
||||
if (! cfg) {
|
||||
throw `Network ${network.name} is not supported`;
|
||||
}
|
||||
|
||||
// Use Alchemy in production, on mainnet.
|
||||
provider = network.name === "homestead" && import.meta.env.PROD
|
||||
? new ethers.providers.AlchemyProvider(network.name, config.alchemy.key)
|
||||
: provider;
|
||||
|
||||
if (! provider) {
|
||||
throw `No Web3 provider available.`;
|
||||
}
|
||||
|
||||
const cfg = (<Record<string, any>> config)[network.name];
|
||||
const ceramic = new CeramicClient(config.ceramic.api);
|
||||
const idx = new IDX({ ceramic });
|
||||
|
||||
const walletConnectState = writable<WalletConnectState>(
|
||||
{ state: "close" }
|
||||
);
|
||||
const wc = Config.initializeWalletConnect(
|
||||
config.walletConnect.bridge,
|
||||
walletConnectState,
|
||||
provider
|
||||
);
|
||||
|
||||
this.network = network;
|
||||
this.metamaskSigner = metamaskSigner;
|
||||
this.walletConnect = {
|
||||
bridge: config.walletConnect.bridge,
|
||||
client: wc.connector,
|
||||
signer: wc.signer,
|
||||
state: walletConnectState,
|
||||
};
|
||||
this.seed = { api };
|
||||
this.registrar = cfg.registrar;
|
||||
this.radToken = cfg.radToken;
|
||||
@@ -70,7 +92,7 @@ export class Config {
|
||||
? new SafeServiceClient(this.safe.api)
|
||||
: undefined;
|
||||
this.provider = provider;
|
||||
this.signer = signer;
|
||||
this.signer = null;
|
||||
this.gasLimits = gasLimits;
|
||||
this.abi = config.abi;
|
||||
this.idx = { client: idx };
|
||||
@@ -83,6 +105,10 @@ export class Config {
|
||||
);
|
||||
}
|
||||
|
||||
setSigner(signer: ethers.Signer & TypedDataSigner | WalletConnectSigner): void {
|
||||
this.signer = signer;
|
||||
}
|
||||
|
||||
// Return the config with an overwritten seed URL.
|
||||
withSeed(seed: string): Config {
|
||||
const cfg = {} as Config;
|
||||
@@ -91,31 +117,109 @@ export class Config {
|
||||
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gas limits for various transactions.
|
||||
const gasLimits = {
|
||||
createOrg: 1_200_000,
|
||||
};
|
||||
getWalletConnectSigner(): WalletConnectSigner {
|
||||
if (this.walletConnect.client.connected) {
|
||||
this.setSigner(this.walletConnect.signer);
|
||||
return this.walletConnect.signer;
|
||||
}
|
||||
const wc = Config.initializeWalletConnect(
|
||||
this.walletConnect.bridge,
|
||||
this.walletConnect.state,
|
||||
this.provider
|
||||
);
|
||||
this.walletConnect.client = wc.connector;
|
||||
this.walletConnect.signer = wc.signer;
|
||||
this.setSigner(wc.signer);
|
||||
|
||||
return wc.signer;
|
||||
}
|
||||
|
||||
static initializeWalletConnect(
|
||||
bridge: string,
|
||||
state: Writable<WalletConnectState>,
|
||||
provider: ethers.providers.JsonRpcProvider
|
||||
): {
|
||||
connector: WalletConnect;
|
||||
signer: WalletConnectSigner;
|
||||
} {
|
||||
const walletConnect = new WalletConnect({
|
||||
bridge: config.walletConnect.bridge,
|
||||
qrcodeModal: {
|
||||
open: (uri: string, onClose) => {
|
||||
state.set({ state: "open", uri, onClose });
|
||||
},
|
||||
close: () => {
|
||||
state.set({ state: "close" });
|
||||
}
|
||||
},
|
||||
clientMeta: {
|
||||
name: "Radicle",
|
||||
description: "Radicle Interface",
|
||||
url: window.location.host,
|
||||
icons: [
|
||||
`${window.location.protocol}//${window.location.host}/favicon.ico`,
|
||||
`${window.location.protocol}//${window.location.host}/favicon.svg`
|
||||
],
|
||||
}
|
||||
});
|
||||
walletConnect.on("modal_closed", () => {
|
||||
state.set({ state: "close" });
|
||||
});
|
||||
|
||||
const walletConnectSigner = new WalletConnectSigner(walletConnect, provider);
|
||||
|
||||
return {
|
||||
connector: walletConnect,
|
||||
signer: walletConnectSigner,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isMetamaskInstalled(): boolean {
|
||||
const { ethereum } = window;
|
||||
return Boolean(ethereum && ethereum.isMetaMask);
|
||||
}
|
||||
|
||||
export async function getConfig(): Promise<Config> {
|
||||
let config: Config;
|
||||
|
||||
if (isMetamaskInstalled()) {
|
||||
const metamask = new ethers.providers.Web3Provider(window.ethereum);
|
||||
const network = await metamask.ready;
|
||||
config = new Config(network, metamask, metamask.getSigner());
|
||||
function getProvider(
|
||||
network: { name: string },
|
||||
config: Record<string, any>,
|
||||
metamask: ethers.providers.JsonRpcProvider | null,
|
||||
): ethers.providers.JsonRpcProvider {
|
||||
// Use Alchemy in production, on mainnet. Otherwise use Metamask if installed.
|
||||
if (network.name === "homestead" && import.meta.env.PROD) {
|
||||
return new ethers.providers.AlchemyProvider(network.name, config.alchemy.key);
|
||||
} else if (metamask) {
|
||||
return metamask;
|
||||
} else {
|
||||
// If we don't have Metamask, we default to Homestead.
|
||||
const network = { name: "homestead", chainId: 1 };
|
||||
config = new Config(network, null, null);
|
||||
throw new Error("No Web3 provider available.");
|
||||
}
|
||||
console.log(config);
|
||||
}
|
||||
|
||||
return config;
|
||||
export async function getConfig(): Promise<Config> {
|
||||
const metamask = isMetamaskInstalled()
|
||||
? new ethers.providers.Web3Provider(window.ethereum)
|
||||
: null;
|
||||
const metamaskSigner = metamask?.getSigner() || null;
|
||||
|
||||
let network = { name: "homestead", chainId: 1 };
|
||||
if (metamask) {
|
||||
// If Metamask is detected, we use the network configured there.
|
||||
network = await metamask.ready;
|
||||
}
|
||||
|
||||
const networkConfig = (<Record<string, any>> config)[network.name];
|
||||
if (! networkConfig) {
|
||||
throw new Error(`Network ${network.name} is not supported`);
|
||||
}
|
||||
|
||||
const provider = getProvider(network, networkConfig, metamask);
|
||||
const cfg = new Config(
|
||||
network,
|
||||
provider,
|
||||
metamaskSigner,
|
||||
);
|
||||
console.log(cfg, "config");
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
"use strict";
|
||||
|
||||
var Buffer = require("buffer/").Buffer;
|
||||
var typedarrayToBuffer = require("typedarray-to-buffer");
|
||||
|
||||
const ENC_UTF8 = 'utf8';
|
||||
const ENC_HEX = 'hex';
|
||||
|
||||
function bufferToArray(buf) {
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
exports.bufferToArray = bufferToArray;
|
||||
|
||||
function bufferToUtf8(buf) {
|
||||
return buf.toString(ENC_UTF8);
|
||||
}
|
||||
exports.bufferToUtf8 = bufferToUtf8;
|
||||
|
||||
function bufferToHex(buf, prefixed = false) {
|
||||
const hex = buf.toString(ENC_HEX);
|
||||
return prefixed ? addHexPrefix(hex) : hex;
|
||||
}
|
||||
exports.bufferToHex = bufferToHex;
|
||||
|
||||
function utf8ToBuffer(utf8) {
|
||||
return Buffer.from(utf8, ENC_UTF8);
|
||||
}
|
||||
exports.utf8ToBuffer = utf8ToBuffer;
|
||||
|
||||
function utf8ToArray(utf8) {
|
||||
return bufferToArray(utf8ToBuffer(utf8));
|
||||
}
|
||||
exports.utf8ToArray = utf8ToArray;
|
||||
|
||||
function hexToArray(hex) {
|
||||
return bufferToArray(hexToBuffer(hex));
|
||||
}
|
||||
exports.hexToArray = hexToArray;
|
||||
|
||||
function hexToBuffer(hex) {
|
||||
return Buffer.from(removeHexPrefix(hex), ENC_HEX);
|
||||
}
|
||||
exports.hexToBuffer = hexToBuffer;
|
||||
|
||||
function removeHexPrefix(hex) {
|
||||
return hex.replace(/^0x/, '');
|
||||
}
|
||||
exports.removeHexPrefix = removeHexPrefix;
|
||||
|
||||
function addHexPrefix(hex) {
|
||||
return hex.startsWith('0x') ? hex : `0x${hex}`;
|
||||
}
|
||||
exports.addHexPrefix = addHexPrefix;
|
||||
|
||||
function arrayToBuffer(arr) {
|
||||
return typedarrayToBuffer(arr);
|
||||
}
|
||||
exports.arrayToBuffer = arrayToBuffer;
|
||||
|
||||
function arrayToHex(arr, prefixed = false) {
|
||||
return bufferToHex(arrayToBuffer(arr), prefixed);
|
||||
}
|
||||
exports.arrayToHex = arrayToHex;
|
||||
|
||||
function concatArrays(...args) {
|
||||
let result = [];
|
||||
args.forEach(arg => (result = result.concat(Array.from(arg))));
|
||||
return new Uint8Array([...result]);
|
||||
}
|
||||
exports.concatArrays = concatArrays;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
var isTypedArray = require('is-typedarray').strict;
|
||||
var Buffer = require('buffer/').Buffer;
|
||||
|
||||
module.exports = function typedarrayToBuffer(arr) {
|
||||
if (isTypedArray(arr)) {
|
||||
var buf = Buffer.from(arr.buffer);
|
||||
if (arr.byteLength !== arr.buffer.byteLength) {
|
||||
buf = buf.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
|
||||
}
|
||||
return buf;
|
||||
} else {
|
||||
return Buffer.from(arr);
|
||||
}
|
||||
};
|
||||
+69
-22
@@ -3,6 +3,7 @@ import type { BigNumber } from 'ethers';
|
||||
import type { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
|
||||
import { Config, getConfig } from "@app/config";
|
||||
import { Unreachable, assert, assertEq } from "@app/error";
|
||||
import * as ethers from "ethers";
|
||||
|
||||
export enum Connection {
|
||||
Disconnected,
|
||||
@@ -29,10 +30,10 @@ export interface Session {
|
||||
}
|
||||
|
||||
export interface Store extends Readable<State> {
|
||||
connect(config: Config): Promise<void>;
|
||||
connectMetamask(config: Config): Promise<void>;
|
||||
connectWalletConnect(config: Config): Promise<void>;
|
||||
updateBalance(n: BigNumber): void;
|
||||
refreshBalance(config: Config): Promise<void>;
|
||||
|
||||
setTxSigning(): void;
|
||||
setTxPending(tx: TransactionResponse): void;
|
||||
setTxConfirmed(tx: TransactionReceipt): void;
|
||||
@@ -41,42 +42,80 @@ export interface Store extends Readable<State> {
|
||||
|
||||
export const loadState = (initial: State): Store => {
|
||||
const store = writable<State>(initial);
|
||||
const session = window.localStorage.getItem("session");
|
||||
const metamask = window.localStorage.getItem("metamask");
|
||||
|
||||
if (session) store.set({ connection: Connection.Connected, session: JSON.parse(session) });
|
||||
if (metamask) {
|
||||
store.set({ connection: Connection.Connected, session: JSON.parse(metamask) });
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
connect: async (config: Config) => {
|
||||
assert(config.signer);
|
||||
connectMetamask: async (config: Config) => {
|
||||
assert(config.metamaskSigner);
|
||||
|
||||
const state = get(store);
|
||||
|
||||
assertEq(state.connection, Connection.Disconnected);
|
||||
assertEq(state.connection, Connection.Disconnected || Connection.Connecting);
|
||||
store.set({ connection: Connection.Connecting });
|
||||
|
||||
// TODO: This hangs on Brave, if you have to unlock your wallet..
|
||||
try {
|
||||
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
const signer = config.signer;
|
||||
const signer = config.metamaskSigner;
|
||||
const address = await signer.getAddress();
|
||||
|
||||
config.setSigner(signer);
|
||||
|
||||
try {
|
||||
config.walletConnect.state.set({ state: "close" });
|
||||
|
||||
const tokenBalance: BigNumber = await config.token.balanceOf(address);
|
||||
const session = { address, tokenBalance, tx: null };
|
||||
|
||||
store.set({
|
||||
connection: Connection.Connected,
|
||||
session,
|
||||
});
|
||||
saveSession({ ...session, tokenBalance: null });
|
||||
saveSession({ ...session });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
connectWalletConnect: async (config: Config) => {
|
||||
store.set({ connection: Connection.Connecting });
|
||||
const signer = config.getWalletConnectSigner();
|
||||
|
||||
try {
|
||||
await config.walletConnect.client.connect();
|
||||
|
||||
const address = await signer.getAddress();
|
||||
const tokenBalance: BigNumber = await config.token.balanceOf(address);
|
||||
const session = { address, tokenBalance, tx: null };
|
||||
const network = await ethers.providers.getNetwork(
|
||||
signer.walletConnect.chainId
|
||||
);
|
||||
|
||||
if (network.chainId !== config.network.chainId) {
|
||||
config.walletConnect.client.killSession();
|
||||
throw new Error(
|
||||
`Network mismatch. Please switch to ${config.network.name}.`
|
||||
);
|
||||
}
|
||||
store.set({ connection: Connection.Connected, session });
|
||||
} catch (e) {
|
||||
store.set({ connection: Connection.Disconnected });
|
||||
|
||||
// There seems to be no way to detect this "error" caused by the user
|
||||
// closing the modal dialog, besides matching on the message string.
|
||||
// Welcome to the wonderful ghetto that is WalletConnect.
|
||||
//
|
||||
// Since it's not really an error, we don't throw if this is what happened.
|
||||
if (e.message !== "User close QRCode Modal") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateBalance: (n: BigNumber) => {
|
||||
store.update((s: State) => {
|
||||
assert(s.connection === Connection.Connected);
|
||||
@@ -168,7 +207,7 @@ export const loadState = (initial: State): Store => {
|
||||
// In case of locking Metamask the accountsChanged event returns undefined.
|
||||
// To prevent out of sync state, the wallet gets disconnected.
|
||||
if (address === undefined) {
|
||||
disconnectWallet();
|
||||
disconnectMetamask();
|
||||
} else {
|
||||
s.session.address = address;
|
||||
saveSession(s.session);
|
||||
@@ -178,11 +217,12 @@ export const loadState = (initial: State): Store => {
|
||||
return s;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const state = loadState({ connection: Connection.Disconnected });
|
||||
|
||||
export const session = derived(state, s => {
|
||||
if (s.connection === Connection.Connected) {
|
||||
return s.session;
|
||||
@@ -193,7 +233,7 @@ export const session = derived(state, s => {
|
||||
window.ethereum?.on('chainChanged', () => {
|
||||
// We disconnect the wallet to avoid out of sync state
|
||||
// between the account address and IDX DIDs
|
||||
disconnectWallet();
|
||||
disconnectMetamask();
|
||||
location.reload();
|
||||
});
|
||||
|
||||
@@ -222,13 +262,20 @@ export async function approveSpender(spender: string, amount: BigNumber, config:
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectWallet(): void {
|
||||
window.localStorage.removeItem("session");
|
||||
location.reload();
|
||||
export function disconnectMetamask(): void {
|
||||
window.localStorage.removeItem('metamask');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function disconnectWallet(config: Config): void {
|
||||
if (config.walletConnect.client.connected) {
|
||||
config.walletConnect.client.killSession();
|
||||
}
|
||||
disconnectMetamask();
|
||||
}
|
||||
|
||||
function saveSession(session: Session): void {
|
||||
window.localStorage.setItem("session", JSON.stringify({
|
||||
window.localStorage.setItem("metamask", JSON.stringify({
|
||||
...session, tokenBalance: null
|
||||
}));
|
||||
}
|
||||
|
||||
+12
-10
@@ -1,26 +1,28 @@
|
||||
import path from "path";
|
||||
import { UserConfig } from "vite";
|
||||
import svelte from "@sveltejs/vite-plugin-svelte";
|
||||
import rewriteAll from "vite-plugin-rewrite-all";
|
||||
import path from 'path';
|
||||
import { UserConfig } from 'vite';
|
||||
import svelte from '@sveltejs/vite-plugin-svelte';
|
||||
import rewriteAll from 'vite-plugin-rewrite-all';
|
||||
|
||||
const config: UserConfig = {
|
||||
optimizeDeps: {
|
||||
exclude: ['svelte-routing']
|
||||
exclude: ['svelte-routing', '@pedrouid/environment', '@pedrouid/iso-crypto']
|
||||
},
|
||||
plugins: [svelte(), rewriteAll()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": path.resolve("./src"),
|
||||
// Polyfill for Node.js "stream" library.
|
||||
"stream": path.resolve("./src/polyfills/stream.ts"),
|
||||
'@app': path.resolve('./src'),
|
||||
// Polyfill for Node.js 'stream' library.
|
||||
'stream': path.resolve('./src/polyfills/stream.ts'),
|
||||
'typedarray-to-buffer': path.resolve('./src/polyfills/typedarray-to-buffer.js'),
|
||||
'enc-utils': path.resolve('./src/polyfills/enc-utils.js'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"process.env": { READABLE_STREAM: "disable" }
|
||||
'process.env': { READABLE_STREAM: 'disable' }
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
outDir: 'build',
|
||||
sourcemap: true
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user