Account Abstraction

Currently, the Verifier contract uses Near AccountId to identify users, which supports both Named and Implicit accounts.

Named Account: user.near

The only way to start using Named Account is to send a real tx intents.near::add_public_key(pk) from user's Near wallet. Others can still deposit/transfer you money before you "claim" it.

Implicit Account

Such accounts already have a public key encoded in their names, so they can be used without any "claiming" from users. There is 1-to-1 relationship between its format and corresponding signing curve types:

  • EdDSA: 8c5cba35f5b4db9579c39175ad34a9275758eb29d4866f395ed1a5b5afcb9ffc (i.e. "Implicit Near")

  • ECDSA: 0x85d456B2DfF1fd8245387C0BfB64Dfb700e98Ef3 (i.e. "Implicit Eth")

This means that if a user logs in with Cosmos (ECDSA) wallet, then he will have an Implicit Eth address inside intents.near, while Solana/Ton (EdDSA) wallets will give you Implicit Near addresses.

It's not feasible to make these addresses different for each chain, since we only know the signature and public key. Even if we differentiate between them based on the used signing standard (NEP-413, EIP-712), then it still leads to ambiguity in case of importing same seed phrase into different wallets.

Account keys

Once an account is created, multiple additional keys could be added to it. Each of these keys has full control over the account and can add or remove other keys either directly via NEAR transactions or via signed intents.

Here is an example of adding public keys for Explicit Near Accounts via tx.

You can also do it manually with near-cli:

near contract call-function as-transaction \
  intents.near add_public_key json-args '{
    "public_key": "ed25519:<base58>"
  }' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
  sign-as <ACCOUNT_ID> network-config mainnet sign-with-keychain send

In addition to directly calling add_public_keymethod it's also possible to submit a signed intent:

{
  "signer_id": "<ACCOUNT_ID>",
  // ...
  "intents": [
    {
      "intent": "add_public_key",
      "public_key": "<PUBLIC_KEY_OF_USER>" 
    },
  ]
}

Authentication Flow example via Frontends

Users' wallets store their private keys and allow users to rotate them. In order to verify signatures inside of Verifier, Verifier should know which keys are associated with which "named" accounts. So, intents.near contract should maintain a copy of mapping of account_ids to their public_keys (again, each account_id can have multiple public_keys registered). This copy should include a subset of valid public keys that were added as Full Access Keys to each NEAR account that wants to interact with Intents.

So, when a user user1 opens the Frontend for the first time and clicks "Connect Wallet", we ask his wallet to signMessage("Authenticate") . As a result, we get a signature and, more importantly, the public_key as a counterpart of a Full Access Key that was used to sign this message and the account_id which the public_key corresponds to. We store this pair (account_id, public_key) in browser's local storage.

Now, when a user wants to swap tokens, i.e. sign a state_change:

  1. Firstly, check if this pair (account_id, public_key) is already registered in Intents contract on-chain by calling get_account_public_keys(account_id) -> Vec<Pubkey> method on intents.near contract.

    1. If there is no such user or if public_key was not registered for him, the user should be "registered" in Intents contract. For that we should ask user's wallet to signMessage("user1 is an owner of public_key ed25519:abcd...")

    2. Then we should send this signed message in the transaction (can be done by relay) to intents.near calling add_public_key({"account_id": "user1", "public_key": "ed25519:abcd...", "signature": "xyz123..."}) method. This transaction would add ed25519:abcd... to the list of public_keys that belongs to user1

  2. Ask user's wallet to signMessage({"account_id": "user1", "state_changes": [...] }) and this state_change along with the returned signature and public_key would be eventually sent by solver to the intents.near.

  3. When intents.near receives transaction with such signature, it validates the signature for given public_key and atomically checks whether the public_key is registered for user1.

Unfortunately, this brings up a new challenge: whenever the user removes his Full Access Key, it should also be unregistered on intents.near contract by calling unregister_key(public_key) method sent from user's NEAR account. We can try to make it for him with a FunctionalKey added to user's account on NEAR and call by ourselves whenever we detect that our user has deleted a key from his NEAR account on-chain.

Last updated