Easier Superhero Wallet Integration in JS

Hello everybody,

in an effort to improve developer experience, we need to improve the integration of (AEX2) Web Wallets like the Superhero Wallet.

long story short, going with the example from the docs I propose we go for something like this

import Ae from '@aeternity/aepp-sdk/es/ae/universal' // or other flavor
import Node from '@aeternity/aepp-sdk/es/node' // or other flavor
import { AE_AMOUNT_FORMATS } from '@aeternity/aepp-sdk/es/utils/amount-formatter'

// New: 
import {AEX2wallet as webwallet} from '@aeternity/foobar/whatever'

const NODE_URL = 'https://testnet.aeternity.io'
const COMPILER_URL = 'COMPILER_URL' // required for using Contract

//old:
const ACCOUNT = MemoryAccount({ keypair: { secretKey: 'A_PRIV_KEY', publicKey: 'A_PUB_ADDRESS' } })

//optional new for web: 
const ACCOUNT = webwallet()

(async function () {
  const nodeInstance = await Node({ url: NODE_URL })
  const sdkInstance = await Ae({
     compilerUrl: COMPILER_URL,
     nodes: [ { name: 'test-net', instance: nodeInstance } ],
// note: allow this to remain an array if possible to prevent errors from people adjusting the existing example for noeJS
     accounts: [ ACCOUNT ]
  })

})()

Edit:

1.What I forgot about is: There must be some callback for the fact that the user rejected the browser wallet connection. Or do we treat it like a “no wallet installed” case ?

  1. If any of the two cases above are true, the SDK shall be set up in a state in which it is not able to do any TX, but perform calls like balance checks and dry-runs (with the default account). Is that even possible right now @davidyuk or do some stamp things require accounts to be always present or so ?
3 Likes

Is this “new”:

import {AEX2wallet as webwallet} from '@aeternity/foobar/whatever'

//optional new for web: 
const ACCOUNT = webwallet()

and this old?


const NODE_URL = 'https://testnet.aeternity.io'
const COMPILER_URL = 'COMPILER_URL' // required for using Contract

const ACCOUNT = MemoryAccount({ keypair: { secretKey: 'A_PRIV_KEY', publicKey: 'A_PUB_ADDRESS' } })

(async function () {
  const nodeInstance = await Node({ url: NODE_URL })
  const sdkInstance = await Ae({
     compilerUrl: COMPILER_URL,
     nodes: [ { name: 'test-net', instance: nodeInstance } ],
// note: allow this to remain an array if possible to prevent errors from people adjusting the existing example for noeJS
     accounts: [ ACCOUNT ]
  })

})()

if not, could you please state two separate examples for how you imagine it?

The “old” variation works if you have the private / public key already, which is not the case. For AEX2 you need a lot more code , see: https://github.com/aeternity/aepp-sdk-js/blob/develop/examples/browser/vuejs/connect-two-ae/aepp/src/components/Home.vue#L330. There are some optional things in there but generally this is what you need to do for AEX2.

In general I agree with the fact, that it should be less boilerplaty and easier to use.

4 Likes

Exactly. I think putting a function where the MemoryAccountsused to reside would be the most elegant solution from a developer perspective, with a way to pass some settings like:

const ACCOUNT = webwallet({stuff: things})

Which optional things were you referring to ?

But if you have a MemoryAccount you don’t need a wallet, it is your wallet, so you are comparing apples to pears.

Regarding the optional things I am referring to this: https://github.com/aeternity/aepp-sdk-js/blob/develop/examples/browser/vuejs/connect-two-ae/aepp/src/components/Home.vue#L330

In frontend-apps, you are not supposed to have memory accounts.

My suggestion to put the “wallet initializer” function to

//optional new for web: 
const ACCOUNT = webwallet()

originates from the thought to tell developers “Hey, no keys go here! Use the web wallet.”, it just would be intuitive to put it there. And as the wallet does not and cannot take any extra information about which node or compiler to talk to (right?), it should be no problem to just fire that function in that place.

I would also like to see an improved flow to setup a wallet connection from an app.
Currently, there is a lot of boilerplate a developer as to use to make it work.

Form my perspective there are two things:

  • We need to keep the existing implementation for developers that want to control the complete flow
  • We need a simple implementation for developer that just use the existing magic

I’m thinking of a function like this:

const wallet = await ConnectedWallet({nodeUrl, internalNodeUrl, confirmation: () => {...}})

The confirmation function should return true in case the wallet should be connected or false in case not. It will reduce the boilerplate code (see https://github.com/aeternity/aepp-sdk-js/blob/develop/examples/browser/vuejs/connect-two-ae/aepp/src/components/Home.vue#L330 ) and improve developer experience (expecially) newcomers.

Furthermore, I agree with @keno.chain that there is a difference between the MemoryAccount and the wallet connected. Let’s use this thread to discuss wallet integration instead of MemoryAccount.
What do you think? @keno.chain @nikitafuchs.chain @davidyuk

3 Likes

Ditch the MemoryWallets itself from all this guys, it’s the place where they were used to be put which is just logically related the most to what’s tried to be done here. You basically tell devs not to hard code keys to have accounts, but “take them from the extension”.

The callbacks are a good thing and could be provided inside here:
const ACCOUNT = webwallet(confirmation: () => {...}})

the good thing about this is that people won’t have to change much about their code, a simple injection of a few more lines and that’s it.

One thing I’m still wondering about on the side though: What is it that you all mean with “leaving things” to be done manually ? Of course we can’t leave nothing to the dAepp-developer to tell the browser wallet. Neither which node URL to use, nor which compiler, nor which network. Just as all this isn’t possible right now, right ?

There are 2 things:

  1. The MemoryAccount: I think there are still valid cases for the MemoryAccount. You only think in terms of a Frontend developer who‘s code lives in a browser. There you don‘t want that keypairs will be entered and use the browser extensions instead. However, the SDK is also used by nodejs backend developers. There might be cases were it still make sense to have a MemoryAccount.

  2. What I mean with „leave things manual“ is related to the AEX2 setup where there is currently a lot of boilerplate. I would like to create a function as part of the SDK which does all the boilerplate and results on only one function call. But to keep it compatible with existing implementations we can’t remove the stuff that is already there. Additionally, there might be cases we miss were developers still want to setup all the AEX2 stuff manually.

One question: when you say „webwallet“ is it still a AEX2 connection to something like a Browser Extension?

Guess I phrased my wording wrong. Memory accounts are all fine in the backend.
Yes, I’m using the term web-wallet synonymously to what metamask does in the Ethereum space.

AEX2 has everything in place that’s necessary to make this a one-liner, saying “Start talking with the browser add-on and call these functions upon certain events regarding the communication with that wallet” - that’s it.

The only thing this has to do with memory accounts is that I suggested placing this code-wise right where we used to define only memory accounts previously, because that spot is logically closely related and requires devs to change and learn next to nothing.

1 Like

“AEX-2” implementation in SDK is overcomplicated, I’m planning to improve it in a near future.

7 Likes

@Denis Davidyuk Just to make sure the meeting conclusion is right. Re writing them here.
Subhod: With Current implementation aepp can never really operate offline without Node information since preparing tx depends on this.client.api.getAccountNextNonce Node API. We propose 2 new methods for the wallet.
1. getAccountNextNonce
2. getNetworkId()

Denis: Apart of this, Aepp needs Node details for other actions like displaying transaction data, querying contacts. Hence Aepp can never stay offline but it can request Node connection details from the wallet and wallet may share it.

Conclusion: Introduce a new method getNodesInPool to the wallet standard. Aepp developers can use this API to get Node details and don’t have to worry about maintaining own nodes. Wallet may wan’t to share the details or reject the request. Aepp shall handle it accordingly.

Draft proposal for simplified wallet integration:

Conventions:

  • SDK instance naming: ae/aex/aeInstance/aex/aeSdk/aeSdkInstance
  • Wallet sub-instance: ae.wallet/aeInstance.wallet/*.wallet

Specification:

The wallet may be connected to Aeternity nodes or it’s offline.
If connected then the wallet may construct the SDK instance using RpcWallet class and Aepp can easily use RpcAepp class without the Node configuration.

wallet = await RpcWallet({
        compilerUrl: compilerUrl,
        accounts: [genesisAccount],
        nodes: [{ name: 'local', instance: node }],
        name: 'Wallet',
        ....
      })

aepp = await RpcAepp({
    name: 'AEPP'
})

If wallet is not connected to any node then aepp can use the SDK class RpcAepp with Node configuration

aepp = await RpcAepp({
    name: 'AEPP',
    nodes: [
      {name: 'ae_mainnet', instance: await Node({url: this.MAINNET_URL})},
      {name: 'ae_uat', instance: await Node({url: this.TESTNET_URL})}
    ],
    compilerUrl: this.COMPILER_URL,)

const signedTx = wallet.sign({tx}) // wallet interaction

aepp.sendTransaction({signedTx}) // Node interaction

Methods

  1. connect: Connection request from aepp to wallet.
    request: {'version': 1}
    response:
    - {'mode': 'bridge', 'networkId': .., 'accounts':[...], node:{Node:{...}
    - {'mode': 'detach', 'accounts':[..]}
    Note on modes.

    1. detach: offline wallet. Node/networkId is determined by the Aepp. Transactions can only be signed and not broadcasted.
    2. bridge: Wallet is connected to Node. Node/networkId is determined by the wallet. Transactions are signed and broadcast to the network.
        import AeWallet from '@aeternity/aepp-sdk/es/ae/wallet'
        import RpcAepp from '@aeternity/aepp-sdk'
    
        const aepp = await RpcAepp({
            name: 'AEPP'})
    
        const wallet = AeWallet.connect(walletInfo)
    
  2. sign: Ask the wallet to sign the transaction.
    request: {'tx': ..., account:..}
    response: {'signature': ..}

  3. sendTransaction: Ask the wallet to sign and broadcast the transaction.
    request: {'tx': ..., account:..}
    response: {'txHash': ..}

        const txHash = wallet.sendTransaction({tx, account: wallet.currentAccount})
    
  4. disconnect: Disconnect wallet

        wallet.disconnect()
    
  5. getNodesInPool: Get Node Information from the wallet. The wallet may share the Node details.

       wallet.getNodesInPool()
    

    aepp can use this Node data to communicate with the desired Aeternity network.

Backwards Compatibility

The existing subscribe/notification methods can be continued to be used in the same format.
Current connect, sign, sendTransaction, disconnect will be marked as deprecated over the new methods.

Edit 1:

Nonce management

It’s important to address how the account nonce will be managed. SDK has an option nativeMode to choose the preferred transaction build platform, use SDK to build if set to true, use Aeternity node otherwise. Current implementation still depends on the node API getAccountNextNonce to build the transaction even when nativeMode is set true.

1 Like

Hey, thanks for the proposal. The intention of simplifying wallet integration though is to make additional code obsolete, unless it’s callbacks to the dApp regarding actions taking place in the wallet (like wallet change, network change, etc.). Like in the original proposal, it is - technically - enough to say “hey SDK, use the wallet if it’s available, else take these accounts” (makes switching testing to production easy). Without going too much into detail here, everything that is in the way of this should and can be removed. Shoot questions if there are questions.

EDIT: Seeing the original proposal doesn’t even account to this for some reason :roll_eyes:will edit a bit later or open a new thread, sorry.

The current plan is to add a new method getNodesInPool() to the wallet standard which will return the node details to the aepp which inturn can be used by the aepp to connect to the nodes.

@subhod-i Hold on, full stop.

When using the wallet, the aepp should not (read: must not) have to connect to any node whatsoever. The full communication to the chain must solely run through the browser wallet, as this is the definitive point of truth to the user and to the app. Plus, we’re not going to expose the node details the user has configured in his wallet to the aepp developers !

1 Like

@nikitafuchs.chain The problem we will be facing is SDK version management. as @davidyuk said, The aepp and wallet version must be in sync. This is gonna be difficult as we release new versions and breaking changes in the SDK. How do you propose the full communication to the chain from aepp to wallet to happen?

2 Likes

And herein lies the full misconception of our initial SDK/wallet design: When using the web wallet, the wallet must become the SDK.

Here again a brief reminder to do market research before developing own solutions and expensively reinventing the wheel :hugs:

Look at what happens in my console once I type ethereum or web3 :

It’s because the Metamask Wallet injects (!) almost everything (!) one needs in order to run commands to call contracts, send TXs, etc pp.

If one relies on some some rarely updated client-side logic (e.g. dApps that rarely update their SDK version) to form transactions and contract calls, this will never work with the latest protocol versions. But if we put this part of the logic into the wallet, all that happens on the client side is importing a JS package (the sdk) and running some basic logic that connects the client application to the wallet.

To put it in other words: All the client (dApp in the browser) must be doing is saying: “Hey browser wallet, I want to send a TX, here is the target address, params and the ACI for encoding/decoding, please do your thing and tell me the outcome, thanks.”

1 Like
Wallet injecting SDK instance RPC based communication: Offline wallet RPC based communication: Online wallet
Easy to get started for aepp developers Secure wallet Easy to get started for aepp developers
Well known technique Aepp depends on own nodes for querying chain and sending transactions. Wallet does only transaction signing Aepp depends on wallet nodes for chain communication
Aepp developers don't have to maintain own nodes Works for future releases since communication standard `RPC` remains same Works for future releases as long as few core method name and prototype remains same
It's never a community based product. A private wallet enterprise product would be great SDK shall provide a standard for RPC communication method `signTransaction` Wallet will not expose Node data but acts as a bridge/Wallet can expose Node data to the Aepp and work in detached mode
Hard for the wallet and aepp to be in sync with the SDK releases. Might lead to impedance mismatch Difficult to get started with this approach Exposing Node data to aepp is not a good idea. It will be a good feature if wallet can provide chain methods via RPC

Current situation: SDK supports only offline wallets. Aepp must have its own Nodes to interact with the chain.

Conclusion: SDK repository can keep the wallet-aepp communication standard and decide on whether to support the online wallet feature(easy for developers to get started). Meanwhile, Superhero wallet can inject an SDK instance to the aepp DOM which is independent of the SDK boundary.

2 Likes

Base points:

  • Wallet cannot expose Node data to the aepp
  • Aepp cannot fully be trusted(Node endpoints). There must be a provision to the aepps to connect to the user’s trusted wallet nodes/own nodes.
  • Deep linking for contract queries leads to poor user experience on smartphones.

AEX-2 Improvements

Keeping existing methods intact, Introduce a new RPC communication function requestNode for the wallet which will forward the aepp requests to the node to which the wallet is connected.

  • requestNode: request wallet for Chain calls.
    request: {
    ‘method’: ‘getTransactionByHash’,
    ‘params’: [‘txHash’]
    }

    response: {
    ‘tx’: ‘txHash’,
    ‘data’: ‘…’,
    ‘from’: ‘account’
    }

Browser-desktop: requestNode method is seamless for the desktop wallets and for the aepps running within the smartphone wallet iframe.
Browser-smartphone: Deeplink for frequent reads from chain leads to poor user experience as users have to switch between wallet and browser applications multiple times. So the proposal is to enable only writing to the chain via the deep link and then reading depends on the aepp(maybe use middleware/public-Nodes/Own-Nodes)

2 Likes