Refactor wallet connect, automatically propose network

This commit is contained in:
Râu Cao
2023-01-20 17:27:29 +08:00
parent 4356288497
commit 1e7d8491f9
10 changed files with 225 additions and 120 deletions
@@ -1,45 +1,37 @@
import Component from '@ember/component'; import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { action } from '@ember/object';
import { isPresent } from '@ember/utils'; import { isPresent } from '@ember/utils';
export default Component.extend({ export default class TopbarAccountPanelComponent extends Component {
@service router;
@service kredits;
tagName: '', @tracked setupInProgress = false;
kredits: service(), get userHasWallet () {
router: service(),
setupInProgress: false,
userHasEthereumWallet: computed(function() {
return isPresent(window.ethereum); return isPresent(window.ethereum);
}), }
showConnectButton: computed('userHasEthereumWallet', get walletConnected () {
'kredits.hasAccounts', function() { return this.userHasWallet && this.kredits.hasAccounts;
return this.userHasEthereumWallet && }
!this.kredits.hasAccounts;
}),
actions: { get walletDisconnected () {
return this.userHasWallet && !this.kredits.hasAccounts;
}
@action
signup () { signup () {
this.router.transitionTo('signup'); this.router.transitionTo('signup');
},
async connectAccount() {
try {
await window.ethereum.enable();
this.set('setupInProgress', true);
await this.kredits.setup();
this.set('setupInProgress', false);
} catch (error) {
this.set('setupInProgress', false);
console.log('Opening Ethereum wallet failed:', error);
} }
@action
async connectWallet () {
this.setupInProgress = true;
await this.kredits.connectWallet();
this.setupInProgress = false;
} }
} }
});
@@ -2,16 +2,15 @@
{{#if this.setupInProgress}} {{#if this.setupInProgress}}
Connecting account... Connecting account...
{{else}} {{else}}
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}} {{#if (and this.walletConnected this.kredits.currentUser)}}
{{this.kredits.currentUser.name}} {{this.kredits.currentUser.name}}
{{#if this.kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}} {{else}}
<span class="anonymous">Anonymous</span> <span class="anonymous">Anonymous</span>
<button onclick={{action "signup"}} class="small" type="button">Sign up</button> <button onclick={{action "signup"}} id="signup"
{{#if this.showConnectButton}} class="small" type="button">Sign up</button>
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button> {{#if this.walletDisconnected}}
<button onclick={{action "connectWallet"}} id="connect"
class="small green" type="button">Connect wallet</button>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
+2 -2
View File
@@ -4,8 +4,8 @@ import config from 'kredits-web/config/environment';
function createStore(name) { function createStore(name) {
let networkName; let networkName;
if (config.web3RequiredNetworkName) { if (config.web3NetworkName) {
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-'); networkName = config.web3NetworkName.toLocaleLowerCase().replace(' ', '-');
} else { } else {
networkName = 'custom'; networkName = 'custom';
} }
+44 -22
View File
@@ -5,7 +5,7 @@ import Service from '@ember/service';
import EmberObject from '@ember/object'; import EmberObject from '@ember/object';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed'; import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
import { isEmpty, isPresent } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { task, taskGroup } from 'ember-concurrency'; import { task, taskGroup } from 'ember-concurrency';
@@ -15,6 +15,7 @@ import processContributorData from 'kredits-web/utils/process-contributor-data';
import processContributionData from 'kredits-web/utils/process-contribution-data'; import processContributionData from 'kredits-web/utils/process-contribution-data';
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data'; import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
import formatKredits from 'kredits-web/utils/format-kredits'; import formatKredits from 'kredits-web/utils/format-kredits';
import switchNetwork from 'kredits-web/utils/switch-network';
import config from 'kredits-web/config/environment'; import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor'; import Contributor from 'kredits-web/models/contributor';
@@ -55,6 +56,21 @@ export default Service.extend({
this.set('contributors', []); this.set('contributors', []);
this.set('contributions', []); this.set('contributions', []);
this.set('reimbursements', []); this.set('reimbursements', []);
if (window.ethereum) {
window.ethereum.on('chainChanged', this.handleUserChainChanged);
window.ethereum.on('accountsChanged', this.handleAccountsChanged);
}
},
handleUserChainChanged (chainId) {
console.log('User-provided chain ID changed to', chainId);
window.location.reload();
},
handleAccountsChanged (accounts) {
console.log('User-provided accounts changed to', accounts);
window.location.reload();
}, },
// This is called in the application route's beforeModel(). So it is // This is called in the application route's beforeModel(). So it is
@@ -64,7 +80,7 @@ export default Service.extend({
let ethProvider; let ethProvider;
return new Promise(resolve => { return new Promise(resolve => {
function instantiateWithoutAccount () { function instantiateWithoutWallet () {
console.debug('[kredits] Creating new instance from npm module class'); console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`); console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl); ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
@@ -74,44 +90,50 @@ export default Service.extend({
}); });
} }
async function instantiateWithAccount (web3Provider, context) { async function instantiateWithWallet (web3Provider, context) {
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask'); console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
ethProvider = new ethers.providers.Web3Provider(web3Provider); ethProvider = new ethers.providers.Web3Provider(web3Provider);
const network = await ethProvider.getNetwork(); const network = await ethProvider.getNetwork();
if (isPresent(config.web3RequiredChainId) && const accounts = await ethProvider.listAccounts();
network.chainId !== config.web3RequiredChainId) { const chainId = config.web3ChainId;
return instantiateWithoutAccount();
}
ethProvider.listAccounts().then(accounts => { if (isEmpty(accounts)) return instantiateWithoutWallet();
if (network.chainId !== chainId) {
return switchNetwork();
} else {
context.set('currentUserAccounts', accounts); context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner(); const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({ resolve({
ethProvider, ethProvider,
ethSigner ethSigner
}); });
}); }
} }
if (window.ethereum) { if (window.ethereum) {
if (window.ethereum.isConnected()) { instantiateWithWallet(window.ethereum, this);
instantiateWithAccount(window.ethereum, this);
} else { } else {
instantiateWithoutAccount(); instantiateWithoutWallet();
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
} }
}); });
}, },
async connectWallet () {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const network = await provider.getNetwork();
const chainId = config.web3ChainId;
const chainIdHex = `0x${Number(chainId).toString(16)}`;
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
if (network.chainId !== chainId) await switchNetwork(chainIdHex);
} catch (err) {
console.log('Connecting wallet failed:', err);
return false;
}
},
async setup () { async setup () {
const kredits = await this.getEthProvider().then(providerAndSigner => { const kredits = await this.getEthProvider().then(providerAndSigner => {
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, { return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
+4
View File
@@ -45,6 +45,7 @@
</div> </div>
</section> </section>
{{#if this.contributionsConfirmed}}
<section id="contributions-by-type"> <section id="contributions-by-type">
<header> <header>
<h2>Contributions by type</h2> <h2>Contributions by type</h2>
@@ -53,6 +54,7 @@
<ChartContributionsByType @contributions={{this.contributions}} /> <ChartContributionsByType @contributions={{this.contributions}} />
</div> </div>
</section> </section>
{{/if}}
</div> </div>
<div id="contributions"> <div id="contributions">
@@ -87,6 +89,7 @@
</section> </section>
{{/if}} {{/if}}
{{#if this.contributionsConfirmed}}
<section id="contributions-confirmed"> <section id="contributions-confirmed">
<header class="with-nav"> <header class="with-nav">
<h2>Confirmed Contributions</h2> <h2>Confirmed Contributions</h2>
@@ -105,6 +108,7 @@
@showQuickFilter={{this.showQuickFilterConfirmed}} /> @showQuickFilter={{this.showQuickFilterConfirmed}} />
</div> </div>
</section> </section>
{{/if}}
{{#if this.showFullContributionSync}} {{#if this.showFullContributionSync}}
<section id="sync-all-contributions"> <section id="sync-all-contributions">
+6 -5
View File
@@ -8,20 +8,21 @@
part in the community's project governance and finances. part in the community's project governance and finances.
</p> </p>
<p> <p>
In order to interact with the system you will need a <a In order to interact with the system you will need a
href="https://rootstock.io">Rootstock</a> wallet/account. <a href="https://rootstock.io" target="_blank" rel="noopener noreferrer">Rootstock</a>
wallet/account.
</p> </p>
<form> <form>
<p> <p>
<label> <label>
Rootstock account:<br> Rootstock address:<br>
<Input @type="text" @value={{this.accountAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} /> <Input @type="text" @value={{this.accountAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
</label> </label>
</p> </p>
</form> </form>
<p class="actions"> <p class="actions">
<button disabled={{this.signupButtonDisabled}} <button {{on "click" (fn this.completeSignup)}}
onclick={{action "completeSignup"}} disabled={{this.signupButtonDisabled}}
type="button"> type="button">
Complete my profile Complete my profile
</button> </button>
+29
View File
@@ -0,0 +1,29 @@
import config from 'kredits-web/config/environment';
export default async function () {
const networkName = config.web3NetworkName;
const chainId = config.web3ChainId;
const chainIdHex = `0x${Number(chainId).toString(16)}`;
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }]
});
} catch (err) {
// This error code indicates that the chain has not been added to MetaMask
if (err.code === 4902) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: chainIdHex,
chainName: networkName,
rpcUrls: [ config.web3ProviderUrl ],
nativeCurrency: { name: 'tRBTC', symbol: 'tRBTC', decimals: 18 }
}]
});
} else {
console.warn('Failed to switch chains:', err.message);
}
}
}
+8 -3
View File
@@ -26,8 +26,8 @@ module.exports = function(environment) {
}, },
web3ProviderUrl: 'https://rsk-testnet.kosmos.org', web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
web3RequiredChainId: 31, web3ChainId: 31,
web3RequiredNetworkName: 'RSK Testnet', web3NetworkName: 'RSK Testnet',
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github', githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github', githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
@@ -82,7 +82,12 @@ module.exports = function(environment) {
if (process.env.WEB3_PROVIDER_URL) { if (process.env.WEB3_PROVIDER_URL) {
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL; ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
ENV.web3RequiredChainId = null; }
if (process.env.WEB3_CHAIN_ID) {
ENV.web3ChainId = parseInt(process.env.WEB3_CHAIN_ID);
}
if (process.env.WEB3_NETWORK_NAME) {
ENV.web3NetworkName = process.env.WEB3_NETWORK_NAME;
} }
return ENV; return ENV;
+1 -1
View File
@@ -17,7 +17,7 @@
"start": "ember serve", "start": "ember serve",
"test": "npm-run-all lint:* test:*", "test": "npm-run-all lint:* test:*",
"test:ember": "ember test", "test:ember": "ember test",
"start:local": "WEB3_PROVIDER_URL=http://localhost:8545 ember serve", "start:local": "WEB3_PROVIDER_URL=http://localhost:8545 WEB3_CHAIN_ID=1337 WEB3_NETWORK_NAME='Hardhat Devchain' ember serve",
"build": "ember build --environment=production", "build": "ember build --environment=production",
"build-prod": "rm -rf release/* && ember build -prod --output-path release", "build-prod": "rm -rf release/* && ember build -prod --output-path release",
"preversion": "npm test", "preversion": "npm test",
@@ -3,37 +3,90 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
function stubWalletApi () {
window.ethereum = {
on: function() { return true; },
request: function() { return Promise.resolve(); }
}
}
module('Integration | Component | topbar-account-panel', function(hooks) { module('Integration | Component | topbar-account-panel', function(hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test('unknown user without wallet (or no permission to get wallet/account info)', async function(assert) { test('Unknown user without wallet', async function(assert) {
await render(hbs`<TopbarAccountPanel />`); await render(hbs`<TopbarAccountPanel />`);
assert.ok(this.element.textContent.trim().match(/^Anonymous/)); assert.ok(
this.element.textContent.trim().match(/^Anonymous/),
'shows current user as anonymous'
);
assert.equal(
this.element.querySelectorAll('button#signup').length, 1,
'signup button is visible'
);
assert.equal(
this.element.querySelectorAll('button#connect').length, 0,
'connect button is not visible'
);
}); });
test('unknown user with Ethereum wallet', async function(assert) { test('Unknown user with disconnected wallet', async function(assert) {
stubWalletApi();
let service = this.owner.lookup('service:kredits');
service.set('currentUserAccounts', []);
await render(hbs`<TopbarAccountPanel />`);
assert.ok(
this.element.textContent.trim().match(/^Anonymous/),
'shows current user as anonymous'
);
assert.equal(
this.element.querySelectorAll('button#signup').length, 1,
'signup button is visible'
);
assert.equal(
this.element.querySelectorAll('button#connect').length, 1,
'connect button is visible'
);
});
test('Unknown user with connected wallet', async function(assert) {
stubWalletApi();
let service = this.owner.lookup('service:kredits'); let service = this.owner.lookup('service:kredits');
service.set('currentUserAccounts', [{ foo: 'bar' }]); service.set('currentUserAccounts', [{ foo: 'bar' }]);
await render(hbs`<TopbarAccountPanel />`); await render(hbs`<TopbarAccountPanel />`);
assert.ok(this.element.textContent.trim().match(/^Anonymous/)); assert.ok(
this.element.textContent.trim().match(/^Anonymous/),
'shows current user as anonymous'
);
assert.equal(
this.element.querySelectorAll('button#signup').length, 1,
'signup button is visible'
);
assert.equal(
this.element.querySelectorAll('button#connect').length, 0,
'connect button is not visible'
);
}); });
test('known contributor', async function(assert) { test('Known contributor', async function(assert) {
stubWalletApi();
let service = this.owner.lookup('service:kredits'); let service = this.owner.lookup('service:kredits');
service.set('currentUserAccounts', [{ foo: 'bar' }]); service.set('currentUserAccounts', [{ foo: 'bar' }]);
service.set('currentUser', { service.set('currentUser', { name: 'Dorian Nakamoto', isCore: false });
name: 'Dorian Nakamoto',
isCore: false
});
await render(hbs`<TopbarAccountPanel />`); await render(hbs`<TopbarAccountPanel />`);
assert.dom(this.element).hasText('Dorian Nakamoto'); assert.dom(this.element).hasText(
'Dorian Nakamoto', 'shows current user\'s name'
service.set('currentUser.isCore', true); );
await render(hbs`<TopbarAccountPanel />`); assert.equal(
this.element.querySelectorAll('button#signup').length, 0,
assert.equal(this.element.querySelectorAll('span.core-flag').length, 1); 'signup button is not visible'
);
assert.equal(
this.element.querySelectorAll('button#connect').length, 0,
'connect button is not visible'
);
}); });
}); });