Sign inSign up

adexnetwork/nft-image-proxy

By adexnetwork

•Updated about 7 hours ago

Image
0

10K+

adexnetwork/nft-image-proxy repository overview

⁠Ambire relayer

The relayer is responsible for managing on-chain Identities (called "Ambire accounts" in public communication) and relaying gasless transactions to the Ethereum network.

It is not intended to be blockchain-agnostic, and it is Ethereum-specific.

⁠Implementation

  • Node.js
  • ethers.js
  • MongoDB
  • no plans to change stack in the future; this is not a performance-critical component, as the bottleneck is the Ethereum blockchain; also, it benefits greatly from all the libraries available for JS/Node.js

⁠Run

npm i  --legacy-peer-deps
# it's important to set NODE_ENV=development cause that tells the relayer to use localhost:1934 for the URLs in the emails
NODE_ENV=development npm start

NOTE: Add rewards collection to adexRelayer database. Two records are needed with _ids "adx-rewards" and "balance-rewards" for the wallet to run properly.

In adex-rewards collection:

{
    "_id" : "adx-rewards",
    "rewards" : {
        "0x0000000000000000000000000000000000000001" : 12345.6789,
        ...
    },
    "updated" : ISODate("2023-1-1T12:00:00.0Z")
}

In balance-rewards collection:

{
    "_id" : "balance-rewards",
    "rewards" : {
        "0x0000000000000000000000000000000000000002" : 420.691234,
        ...
    },
    "updated" : ISODate("2023-1-1T12:00:00.0Z")
}

NOTE: Your MongoDB GUI might not recognise it as JSON. In this case remove the "updated" field for temporary fix.

⁠Build (docker)

docker build . --tag=adexnetwork/relayer

Note that there's automatic building set up for adexnetwork/relayer.

⁠Environment variables

  • PRIVATE_KEY
  • PORT
  • MONGO_URL
  • MONGO_DB_NAME
  • EMAIL_SOURCE: set to any email source configured in Amazon SES, in order to enable notification emails; See https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html⁠ on how to load AWS credentials; recommended value is [email protected]
  • AWS_REGION: AWS region
  • OCTOPUSEMAIL_API_URL: Еmail Оctopus API endpoint
  • OCTOPUSEMAIL_API_KEY: Еmail Оctopus API Key
  • OCTOPUSEMAIL_LIST_ID: Еmail Оctopus listID for welcome list
  • LOCK_INTERVAL: Lock interval in seconds for brute force prevention (default is 60 seconds)
  • LOCK_TRIES: Available tries per minute before lock (default is 20 tries)
  • RATE_LIMIT_ENABLE: If the env is set, rate limits will be applied to all routes in the service.

For npm run monitor only:

  • WALLET_ADDR
  • PUSHOVER_USER
  • PUSHOVER_TOKEN

⁠Caveats

  • If the relayer has not ran for some time, and there are no pending transactions, you can drop the counters collection from MongoDB to avoid having to go through all blocks until the latest block

⁠Create grants

node ./scripts/grant.js [mainnet|goerli] [DAI|SAI|TST] amount email

Or, if you want to send invitation emails, use:

[email protected] node scripts/grant.js [mainnet|goerli] [DAI|SAI|TST] amount email

For example:

[email protected] node scripts/grant.js mainnet SAI 5 [email protected]

You can also invite users (send them the email without issuing a grant) using:

[email protected] node scripts/invite.js email

⁠Functionalities

  • Send emails to users when they create an account, receive a grant or are invited
  • Create Identity contracts
  • Counterfactual accounts: keep deploy data for Identities, and only deploy them upon the first blockchain action
  • Distribute grants through coupons
  • Relay gasless transactions to the Identity contract
  • Run routines on the Identity contract (withdraw from channel)
  • Ambire recovery service

For more information on what those mean, see https://github.com/AmbireNetwork/adex-protocol/issues/10⁠

⁠Send emails

Send emails after successfully creating Identity contracts, that inform the user that, for example "Your account has been created"

⁠Create Identity contracts

Each Ambire account (publisher/advertiser) will have their own Identity contract, which is similar to a multi-sig contract that allows this user to interact with the network through various devices/methods of authentication (e.g. phone, browser, Trezor, Ledger). All those keypairs (called "owners") can have different privilege levels: Routines, Transactions and Withdraw.

The Routines privilege level allows to withdraw funds from OUTPACE channels to the Identity contract.

The Transactions privilege level allows any arbitrary transaction to be sent from the Identity contract.

The Withdraw privilege level allows lower privilege levels (Routines/Transactions) to withdraw from the Identity contract to the address with Withdraw privileges. You may want to use that with a hardware wallet (e.g. Trezor), thereby being able to withdraw to your Trezor without having it plugged in.

⁠Counterfactual accounts

The Identity contract for each user only gets deployed upon their first interaction with the blockchain.

The address of this contract is pre-calculated, meaning you can start receiving funds. Once you want to withdraw those funds or start a campaign ("first interaction with the blockchain"), the Identity contract will be deployed.

This is a UX advantage because you do not need to pay any blockchain fees (in this case, to deploy the Identity contract) to start using Ambire. For the tech decisions, see the rough notes⁠, changes in adex-protocol-eth⁠ and and the changes report⁠.

⁠Distribute grants, through coupon codes

The relayer allows grant winners⁠ to claim their pre-funded accounts.

Grant distribution happens upon confirming your account's email, by simply sending DAI to the identity address.

⁠Relay gasless transactions

Any transaction can be executed through the Identity contract, as long as the authorized owner has signed it, via the execute method.

That way, the Relayer can pay the gas for executing this transaction, while being rewarded with tokens for doing so.

This allows owners that don't have ether to interact with the Identity contract.

⁠Run routines

Any owner with a Transactions (or higher) privilege can authorize the relayer to perform "chores" on their behalf. For example, withdrawing from channels, or opening campaigns.

This is similar to transactions, but includes a limited set of operations. For example, withdrawing OUT of the Identity contract is not possible, but withdrawing from channels INTO the Identity contract (called "sweeping") is allowed.

The most powerful advantage here is enabling automatic channel withdrawals: since each OUTPACE channel has a validUntil date, you have to withdraw your earnings before that time. The relayer will do this for you.

⁠The Ambire recovery service

NOTE: this is not implemented yet; instead, we use a fixed HW wallet address that can recover accounts

The Ambire platform will allow "Quick signup", which is a signup mode that only requires a valid email and creates an in-browser keypair automatically (called "Quick accounts" or "Quick wallets").

The recovery service will be automatically added as an owner to each Identity that's created with an in-browser keypair as an owner only.

This way, if the user loses this keypair, the recovery service can intervene, if the user proves control of the email they registered with.

This works the same way as any classic email/password-based login. When/if the user adds a proper authentication method to their Identity (Metamask, Trezor, Ledger), the recovery service will be removed. Alternatively, the user can choose to remove the recovery service themselves, accepting the risk of losing their funds.

Later on, this can be replaced by a trust-minimized DKIM signature-based solution⁠.

⁠Internals

⁠Address convention

All addresses are stored checksummed, no exceptions.

⁠Constants

Defined in services/eth/transactions:

GAS_PRICE_RATIO - the network gas price will be multiplied by this to get the gas price we will use

GAS_PRICE_RATIO_RESUBMIT - the same as GAS_PRICE_RATIO, but used when resubmitting

RESUBMIT_STALLED - boolean - whether re-submitting stalled transactions is enabled

TIME_BEFORE_RESUBMIT - how long do we wait before we re-submit transactions

⁠Methods

Those basic methods would be exposed through REST routes.

⁠Create identity

When you create a new user identity, submit it's original deploy data via POST /identity/:identity.

Then, the relayer will deploy it on one of the following conditions:

  • an execute is requested
  • an executeRoutines is triggered by the routineAuthsLoop
⁠Request transaction execution

execute ({ identityAddr, txnsRaw, signatures, channel })

This method should also evaluate if all signers in the transactions have >=PrivilegeLevel.Transaction privilege level on the Identity contract

If the identity is not deployed, this method will deploy it via the data in the identityDeployData collection. The required fee for the first transaction will be higher to offset the deploy cost.

⁠Routes

POST /identity/:identity - stores an identity to the identityDeployData collection; optionally, you can pass email, which will send a welcome email to the user and a confirmation link (which may trigger receiving a grant, in the form of DAI sent to :identity); does not deploy an identity, it simply stores it; will calculate the expectedAddr and return an error if it's different from :identity

GET /identity/:identity/:network - retrieves up to date information about an identity, including it's current privileges; if it's not deployed yet it will return the initial owners (original privileges); would also return isLimitedVolume

GET /identity/:identity/confirm-email/:code - confirms the email for an identity; optional query parameter ?redirect for redirecting the user somewhere after the confirmation; it will also claim all non-claimed coupons for that email; you can use this URL as many times as you wish, even if the email has already been confirmed - which is useful in case you want to claim new coupons

GET /identity/:network/by-owner/:owner - get a list of identities for which the owner address is privileged

GET /email/:email - returns this email is existing ot not

@TODO update this and add others

⁠Architecture

⁠Executors

Executors are responsible for broadcasting individual user bundles to the network, by translating them to actual Ethereum transactions. Executors are also responsible for marking the bundles as confirmed by the network in case they are, marking them as dropped, nonce management, etc.

Initially we started with one executor, mempool, and we later replaced it with mempool V2. Both call the standard RPC method eth_sendTrarnsaction to broadcast transactions, howeverwe can use a privacy preserving RPC such as Flashbots or MEVProtect.

In order to develop a new executor, one should simply develop a new class that follows the MempoolV2`` interface. There is just one public method, processBundles, and the constructor follows this signature: constructor (executorId: string, network: NetworkInfo, privateKey: string)`

Here are some rules that MempoolV2 and the new executor design in general follow:

  • one bundle = one txn because one etherscan txn and for simplicity: right now, a single user bundle gets "translated" into a single real EVM transaction; this is done because it's much simpler to manage userTxn replacement and re-broadcasting that way, but also because it's much more user-friendly not to batch the processing of bundles of multiple users; the current throughput is way to low for this to be a significant benefit, and even in a positive growth scenario it might stay that way
  • unlike the previous executor system, there's no separate send that the executor returns: because what we write in the DB can be influenced by sending outcome (in case it fails); also, we no longer do DB update before sending (doesn't make sense, we can do the DB update regardless of whether sending failed still)
  • executors don't need to use their own DB collections, because they can use dbUpdate to "stick" stuff to the userTxns collections, and they'll receive all pending bundles (userTxns) at every cycle (every block), so it's more than enough to store needed context
⁠Scheduler

NOTE: updated for Ambire v2

The scheduler is responsible for picking an executor for each bundle and periodically checking the status of each bundle by calling the executor.

It does this on each new block.

It is also responsible for making sure the same executor stays assigned to a user as long as that user has a pending bundle - this is because an executor needs context for all the user's pending bundles to function properly (rpelace-by-fee, enqueuing, etc.)

⁠Security

NOTE: updated for Ambire v2

The relayer will load a single ethers.js signer that it will use globally. This signer will be indirectly accessible to the routes code through the executors.

Since the routes need to issue transactions, there's no going around this, and the routes code should be carefully crafted to not allow any arbitrary code execution/arbitrary transactions being built.

Even if we tried to separate concerns and only sign in a separate worker, the worker would be blindly getting and signing raw transactions directly from the database, so there is little benefit to introducing this.

⁠Scalability and limitations

NOTE: updated for Ambire v2

The relayer is designed to operate with one Ethereum keypair and execute as many transactions per Ethereum block as possible. Because Ethereum will always be the bottleneck by a long shot, we don't need multiple workers or multiple keypairs.

Something very important is that a single executor instance MUST only processBundles once at a time. Currently, this is implemented via a local p-limit, but if we want to scale the relayer to multiple processes we MUST re-implement this lock to be global via Redis or some alternative.

In case this limitation becomes a bottleneck (really unlikely), the simple way to solve it is via horizontal scaling (#465) - in other words, just adding another executor for the network. The scheduler will manage this and ensure we stick to the same executor for a user as long as that user has a pending bundle.

⁠Mempool V2 executor

The mempool V2 executor is re-designed from the ground up with the following benefits:

  • is simpler and more robust
    • relies strongly on network nonecs and implements consistency checks all around. Based on the lessons learned from the first mempool executor
  • does not have or need any extra MongoDB collections
  • written in TypeScript, cleaner and compiler-driven code
  • is faster and makes less RPC calls
    • processes all bundles at once, allowing parallel processing too
    • uses ethers v6 so it auto-batches RPC calls

⁠Database

⁠Transactions

This collection contains transactions that will be executed on-chain, by the relayer.

The userTxns collection has the following fields:

  • txId: optioinal string: the EVM transaction ID, set only if it's broadacasted on the network
  • executed.success: optional boolean: only set if the underlying transactiton has been confirimed by the network
  • TODO

There are additional executor-specific fields, like mempoolV2Current, mempoolV2Pending; TODO

Please note, "executed" object being set simply means the transaction is confirmed on-chain, regardless of whether it was successful or not (e.g. it reverted).

⁠IdentityDeployData

Stores the initial deploy data of each identity.

  • _id: indexed, unique: the address of the identity; calculated by using generateAddress2(identityFactoryAddr, salt, bytecode)
  • salt: indexed: the salt, stored as a hex string
  • bytecode: the compiled bytecode, stored as a hex string; generated from baseIdentityAddr, privileges, routineAuthorizations, but it is stored because we may use a different solc version at some point
  • identityFactoryAddr: the address of the factory we're about to deploy with

Some fields are the underlying arguments passed to getProxyDeployByteCode:

  • baseIdentityAddr: the address of the base identity contract; this may be used in the Platform to identify whether the identity is v4.0 or v4.1
  • initialPrivileges: the wallets that control this identity; passed as privileges

NOTE: deploy fees are not included in this, since we don't need them with counterfactual deployment; since we only deploy upon executing transactions, we can just raise the first execute fee to offset the cost of deploying

Some fields store extra information:

  • initialPrivilegesAddrs: indexed: same as privileges but only the addresses, for indexing purposes
  • meta: unused for now

⁠Debugging transactions with HEVM

When a transaction fails for seemingly mysterious reasons, HEVM can be used to see details like the call stack, which contract it failed in, etc.

Download HEVM from: https://github.com/dapphub/dapptools/releases⁠

Use HEVM like this, make sure to replace all the template values and the RPC if needed:

export TERMINFO=/lib/terminfo
./hevm exec --caller <txn from> --address <txn to> --calldata <txn data> --rpc http://165.22.74.127:8545 --gas 1000000 --debug

⁠Enabling Authenticator OTP for email/passphrase accounts

Currently there's no UI for this, but you can:

const secret = require('otplib').authenticator.generateSecret()
console.log(secret)
db.identities.update({ 'meta.email': 'email' }, { $set: { 'secrets.otp': secret } })

To use this setup, you can add the secret to Google Authenticator, Microsoft Authenticator, Yubikey (Yubico Authenticator app)

⁠Coingecko script for update coin information

This script will update token settings in database.

node scripts/update-coin-price-sets.js

Please, when adding a token for update, leave all previous ones to keep history.

WARN: All defined parameters for update will OVERWRITE previous value for the same parameters !!!

To append instand of overwrite you can use notation in string. Example: 
    [Append] { "param.subparam" : "value" }
    [Overwrite] { param : {subparam : "value"} }

updateInfo[coingeckoId] - coingeckoId is id from document.

When script is finish will automaticaly exit and provide result from mongodb operations.

⁠Velcro (Palance provider API)

This service can be start with

npm start velcro

API end point and balance method

https://velcro.ambire.com/v1/protocols/${protocol}/balances?addresses=${address}&network=${chain}
  • protocol can be tokens or nft
  • address is wallet address
  • chain is chain name from cfg
⁠dump tokensInfo form mongoDb
node scripts/token-info-velcro-dump.js > ./src/velcro/data/tokensInfo.json
⁠restore tokensInfo from file to mongodb
node scripts/token-info-velcro-update.js
⁠Update contract/token info (generates humanizerInfo.json)
node scripts/generateContractInfo.js > src/lib/humanizerInfo.json
⁠Banners

All banners are configurable from cfg/promos.js

campaigns type:

{
    id, // string and should be unique
    title, // string
    text, // string, also allow to include resources as their param title ex: 'Click here: ${{link_for_tests}}' result will be "Click here: Read now!"
    icon, // string emoji
    type, // string with opitions 'info' or 'warning'
    resources: {
        link_for_tests: {
            href: 'https://blog.ambire.com/ambire-wallet-2nd-birthday/',
            label: 'Read now!'
        }
    },
    period: { from: new Date(), to: new Date(), timer: false } // timer: true will show a banner as countdown
}

campaignsCustomData

In this object can be provided campaign id as param and array of specific Ambire addresses. The specific banner will be visible only for those addresses.

⁠EmailVault

For more information checkout https://github.com/AmbireTech/ambire-common/wiki/Email-Vault-Documentation⁠

EmailVault (EV) is collective title for everything related to email accounts in Ambire. In the context of the relayer, EVs are data records in the emailVaults collections, responsible for managing email accounts. The main point of this is to extract all email-related data from the identities collection and to provide flexibility in storing other types of data, related only to EVs.

⁠Each EV stores:
  • the email of the user
  • List of secrets:
    • recoveryKey (key address for the smart account, used for recovery of the account)
    • keyStoreSecret (secret piece of data, used to decrypt a device's keystore, used for device password change/recovery)
  • list operations
    • keystoreSync (used for securely sending private keys between two devices via the relayer)
⁠Secrets

Secrets are used for storing sensitive data in the relayer. Secret structure:

{
  key: string
  value?: string
  type: string
  password?: string
}
⁠Operations

Operations are used for cross-device communication, executed via the relayer Operation structure:

{
  key: string
  value?: string
  type: string
  password?: string
}

Tag summary

Content type

Image

Digest

sha256:a58f2aa8d…

Size

737.2 MB

Last updated

about 7 hours ago

docker pull adexnetwork/nft-image-proxy