Skip to content
Last update: January 26, 2024

Upgrading to the Ethereum Custodian API from a custom integration

Some custodians have created integrations with MMI before the Ethereum Custodian API was available.

Main differences

MMI distinguishes several custodian types which are visible in our source code. Custom integrations each have their own type, but Ethereum Custodian APIs have a type corresponding to their version. (Currently JSONRPC for ECA-1 and ECA3 for ECA-3. Future versions will be named similarly to ECA-3.)

The essence of the Ethereum Custodian API is that the same codebase is used in MMI to interact with all custodians.

Some of the differences described below will compare ECA1 interactions with ECA3 interactions - you may take these to refer to the equivalent interactions in your custom integration.

API

The API is based around JSON-RPC rather than the REST-like APIs used in custom integrations. The API is documented in the Custodian JSON-RPC API playground. We maintain a test custodian here

  • Saturn Custody uses ECA-1
  • Neptune Custody uses ECA-3 (you can switch to Neptune with the Switch to Neptune button)

Authentication

The authentication flow is documented here. The main difference is that the refresh token is now exchanged for an access token at the /auth/token endpoint. The access token is then used to authenticate requests to the custodian JSON-RPC API.

Features present in ECA-3 but not in custom integrations

custodian_listAccountsSigned

Custodians will implement the custodian_listAccountsSigned method, which will return a signed message containing the list of accounts. The message will be signed by the same key as the customer proof.

Example Request

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "custodian_listAccountsSigned",
  "params": []
}

Example Response

{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "jwt" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.W3sibmFtZSI6IkFjY291bnQgMSIsImFkZHJlc3MiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ0YWdzIjpbeyJuYW1lIjoiYWNjb3VudC1uYW1lIiwidmFsdWUiOiJBY2NvdW50IDEifSx7Im5hbWUiOiJ3YWxsZXQtbmFtZSIsInZhbHVlIjoiTXkgRXRoZXJldW0gV2FsbGV0In1dfV0.f4KozAe2XxKqx8m7bpl484qsi9q7TypDhN-R4rlpI14"
  }
}

Additional account properties

ECA-3 adds new properties to the response of custodian_listAccounts. It is not planned that these be added to the response of custodian_listAccountsSigned as well.

{
  "id": 1,
  "jsonrpc": "2.0",
  "result": [
    {
      "name": "Account 1",
      "address": "0x0000000000000000000000000000000000000000",
      "tags": [
        {
          "name": "account-name",
          "value": "Account 1"
        },
        {
          "name": "wallet-name",
          "value": "My Ethereum Wallet"
        }
      ],
        "metadata": {
           "active" : true, // Whether an account is active (can be used with MMI)
           "deleted" : false, // Whether an account exists anymore (in some sense)
           "isContract" : false // Whether the account is a smart contract
        }
      ]
    }
  ]
}

Decoupled Custodial Transaction Publication

ECA-3 makes publishing transactions optional. Some custodians will be configured (in the MMI backend) to publish transactions, and also, MMI can switch off transaction publication for a specific transaction so that it will be published by the Transaction Publisher Service.

Two new properties to be sent with custodian_createTransaction in the metadata object:

custodianPublishesTransaction - whether the custodian should publish the transaction. The value of this should be the same as the value of the custodianPublishesTransaction property in the configuration API. rpcUrl - the user’s preferred RPC URL. This should be the same as the RPC URL for the network corresponding to the chain ID in the transaction proposal.

Example request

  "id": 1,
  "jsonrpc": "2.0",
  "method": "custodian_createTransaction",
  "params": [
    {
      "from": "0xb2c77973279baaaf48c295145802695631d50c01",
      "to": "0x57f36031E223FabC1DaF93B401eD9F4F1Acc6904",
      "type": "0x2",
      "value": "0x1",
      "gas": "0x5208",
      "maxFeePerGas": "0x59682f0e",
      "maxPriorityFeePerGas": "0x59682f0e"
    },
    {
      "chainId": "0x4",
      "originUrl": "https://www.example.com",
      "note": "This is a note to trader",
      "transactionCategory" : "simpleTransfer", // Note this is missing from the example in the old spec
      "custodianPublishesTransaction" : true, // Whether the custodian should publish the transaction
      "rpcUrl" : "https://mainnet.infura.io/v3/12345" // The user's preferred RPC URL. This can be omitted if the custodian supports publication but not custom URLs
    }
  ]
}

The custodian_getTransactionById method must also be altered to contain the custodianPublishesTransaction property, as well as the signedRawTransaction for situations where the custodian does not publish the transaction. The extension is not expected to publish it, so it is not strictly necessary to include the signedRawTransaction for polling responses, but it will aid debugging and adds symmetry to the webhook and polling responses.

We also add chainId to the response, since it is implicit in the signature, as well as present in the webhook.

Example response

{
  "id": 1,
  "jsonrpc": "2.0",
  "result":{
    "transaction" : {
        "id": "ef8cb7af-1a00-4687-9f82-1f1c82fbef54",
        "type": "0x2",
        "from": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
        "to": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52",
        "value": "0x0",
        "gas": "0x5208",
        "gasPrice": "0x4A817C800",
        "nonce": "0x1",
        "data": "0x",
        "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "status": {
            "finished": true,
            "submitted": true,
            "signed": true,
            "success": true,
            "displayText": "Mined"
        },
        "signedRawTransaction" : "0xf86b018504a817c80082520894b8c77482e45f1f44de1745f52c74426c631bdd5280b844a9059cbb000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826"
    },
    "metadata" :  {
      "chainId": "0x4",
      "custodianPublishesTransaction" : true, // Hypothetically, this could change from the original request, e.g. if the custodian does not support publishing but accepts the proposal
      "rpcUrl" : "https://mainnet.infura.io/v3/12345" // The URL to which the transaction will be published. This can be omitted if none was originally sent
    }
  }
}

Replacement transactions

A new method is added, custodian_replaceTransaction, which will replace a transaction that has not yet been mined. This can be used to cancel and speed up transactions

Example request

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "custodian_replaceTransaction",
  "params": [{
      "transactionId" : "ef8cb7af-1a00-4687-9f82-1f1c82fbef54",
      "action" : "speedUp"
    }, 
    {
      "gas" : "0x5208",
      "maxPriorityFeePerGas" : "0x59682f0e",
      "maxFeePerGas" : "0x59682f0e"
    }
  ]
}