decentralized-identity / veramo

A JavaScript Framework for Verifiable Data
https://veramo.io
Apache License 2.0
445 stars 133 forks source link

Allow signed `addKey`/`addService` return type for later submission by other agent #1373

Closed radleylewis closed 5 months ago

radleylewis commented 7 months ago

Is your feature request related to a problem? Please describe. We need to invoke the below methods (using ethr-did-provider) which result in writes to the network:

Describe the solution you'd like I am open to suggestions on how best to approach this issue. One approach would be to allow an option/flag that would not submit the transaction to the network, but rather return the prepared (signed) transaction for submission in another context (i.e. via a Veramo agent instantiated in the DApp invoking). The addKey method looks like the below (did-ethr):

  async addKey(
    { identifier, key, options }: { identifier: IIdentifier; key: IKey; options?: TransactionOptions },
    context: IRequiredContext,
  ): Promise<any> {
    const ethrDid = await this.getEthrDidController(identifier, context)
    const usg = key.type === 'X25519' ? 'enc' : 'veriKey'
    const encoding = key.type === 'X25519' ? 'base58' : options?.encoding || 'hex'
    const attrName = `did/pub/${key.type}/${usg}/${encoding}`
    const attrValue = '0x' + key.publicKeyHex
    const ttl = options?.ttl || this.ttl || 86400
    const gasLimit = options?.gasLimit || this.gas || DEFAULT_GAS_LIMIT
    if (options?.metaIdentifierKeyId) {
      const metaHash = await ethrDid.createSetAttributeHash(attrName, attrValue, ttl)
      const canonicalSignature = await EthrDIDProvider.createMetaSignature(context, identifier, metaHash)

      const metaEthrDid = await this.getEthrDidController(identifier, context, options.metaIdentifierKeyId!)
      debug('ethrDid.addKeySigned %o', { attrName, attrValue, ttl, gasLimit })
      delete options.metaIdentifierKeyId
      const txHash = await metaEthrDid.setAttributeSigned(
        attrName,
        attrValue,
        ttl,
        { sigV: canonicalSignature.v, sigR: canonicalSignature.r, sigS: canonicalSignature.s },
        {
          ...options,
          gasLimit,
        },
      )
      debug(`ethrDid.addKeySigned tx = ${txHash}`)
      return txHash
    } else {
      debug('ethrDid.setAttribute %o', { attrName, attrValue, ttl, gasLimit })
      const txHash = await ethrDid.setAttribute(attrName, attrValue, ttl, undefined, {
        ...options,
        gasLimit,
      })
      debug(`ethrDid.addKey tx = ${txHash}`)
      return txHash
    }
  }

The returned (prepared and signed but not submitted) return type would look something like the below:

return {
  attrName,
  attrValue,
  ttl,
  gasLimit,
  { sigV: canonicalSignature.v, sigR: canonicalSignature.r, sigS: canonicalSignature.s },
  options,
}

Describe alternatives you've considered The approach that I have outlined above would necessitate the implementation of a new function (e.g. submitAddKey) so I am keen to hear other approaches.

Additional context Any method that invokes the eth_sendTransaction is going to result in this issue with errors corresponding broadly to the below example:

Error#1: could not coalesce error (error={ "code": -32601, "data": { "method": "eth_sendTransaction" }, "message": "The method does not exist / is not available." }, payload={ "id": 7, "jsonrpc": "2.0", "method": "eth_sendTransaction", "params": [ { "data": "0x7ad4b0a4000000000000000000000000ab0207f2084bafd9814fd0cf9a3736adf03f117b6469642f7075622f456432353531392f766572694b65792f686578000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000000002007cf5bc93cc16d1559ac91b05e444bc59b554cb39d9f7cc6d1f7f2c6ddde2bd9", "from": "0xab0207f2084bafd9814fd0cf9a3736adf03f117b", "gas": "0x186a0", "to": "0x03d5003bf0e79c5f5223588f347eba39afbc3818" } ] }, code=UNKNOWN_ERROR, version=6.10.0)
radleylewis commented 7 months ago

Building on the above, the amended addKey method would take an option (on the existing options object of signOnly: boolean. If set to true, then the didManagerAddKey (via the provider addKey method would return an object that looks like the following TypeScript tuple type (which conforms to the function parameters of metaEthrDid.setAttributeSigned):

type TxnParams = [
    attrName: string,
    attrValue: string,
    ttl: number,
    signature: { sigV: number; sigR: string; sigS: string },
    options: Record<string, any>,
];

In the introduced didManagerSubmitTransaction the function signature would then look like the following:

async didManagerSubmitTransaction(args: TxnParams, context: IAgentContext<IKeyManager>): Promise<unknown>;

This method (didManagerSubmitTransaction) can then be called from another context (in an agent not constrained by the snap environment) referencing the options.metaIdentifierKeyId for submission to the network.

async didManagerSubmitTransaction(args: TxnParams, context: IAgentContext<IKeyManager>): Promise<any> {
  // this method invokes a new provider method, which means that the calling agent calls `metaEthrDid.setAttributeSigned`
  // with the provided txnParams (that were returned from the agent call in the first invocation within the MM Snap. 
  return provider.submitTransaction(args, context); // additional function made available on `did-ethr-provider`
}
radleylewis commented 2 months ago

Providing an update for the sake of completeness. In our case, the concern was that the eth_sendRawTransaction method was not supported within the context of the MetaMask Snap, and therefore, a workaround (such as signing the transaction and submitting via a proxy such as Infura, or on a server) was proposed.

However, the eth_sendRawTransaction method has since been unblocked by the MetaMask Snaps developers (refer to this PR), and as such, the above, while still relevant in some scenarios no longer applies in the case of MetaMask Snaps.