intrepidshape/elm-web3/3.0.0

Web3.Sign

EIP-712 typed data signing and EIP-191 personal signing.

Build a typed data request, encode it for the JS port, decode the signature response. Use the SignState machine to track the lifecycle of a single sign request.

import Dict
import Json.Encode as E
import Web3.Sign as Sign


permitRequest : T.Address -> T.Address -> BigInt -> Int -> Sign.TypedData
permitRequest owner spender value nonce =
    Sign.typedData
        { domain =
            { name = Just "MyToken", version = Just "1"
            , chainId = Just 369, verifyingContract = Just tokenAddress
            , salt = Nothing
            }
        , types =
            Dict.fromList
                [ ( "Permit"
                  , [ { name = "owner",    typeName = "address" }
                    , { name = "spender",  typeName = "address" }
                    , { name = "value",    typeName = "uint256" }
                    , { name = "nonce",    typeName = "uint256" }
                    , { name = "deadline", typeName = "uint256" }
                    ]
                  )
                ]
        , primaryType = "Permit"
        , message = E.object [ ... ]
        }


-- Send via port:
web3Cmd (Sign.encode "permit-1" signerAddress (permitRequest owner spender value nonce))


-- Receive via port (use signatureDecoder for the raw sig string):
case D.decodeValue Sign.signatureDecoder incoming of
    Ok sig -> -- sig is the 0x-prefixed signature
    Err _ -> -- not a sign response

Sign state machine -- tracks one in-flight request:

type SignState
    = SignIdle
    | SignPending String          -- id
    | Signed String String        -- id, signature
    | SignFailed String String    -- id, error
    | SignRejected String         -- id


-- Usage:
( { model | signState = Sign.startSign "permit-1" model.signState }
, web3Cmd (Sign.encode "permit-1" addr request)
)


-- On port response:
newSignState = Sign.signUpdate signMsg model.signState

An EIP-712 typed data signing request.

type alias Domain =
{ name : Maybe String
, version : Maybe String
, chainId : Maybe Int
, verifyingContract : Maybe Address
, salt : Maybe String
}

EIP-712 domain separator parameters. All fields are optional; include only those your contract's domain hash covers.

type alias TypeField =
{ name : String
, typeName : String
}

A single field in a struct type definition.

{ domain : Domain
, types : Dict String (List TypeField)
, primaryType : String
, message : Value
}

Construct a TypedData value.

encode : String -> Address -> TypedData -> Value

Encode a TypedData request as a port command.

web3Cmd (Sign.encode "my-id" signerAddress request)

The id is echoed back in the signed response so you can match responses to requests when multiple signing requests are in flight.

personalSign : String -> Address -> String -> Value

Sign an arbitrary message with personal_sign (EIP-191).

Used for login flows and simple off-chain authentication. The wallet will display the raw message to the user before signing.

web3Cmd (Sign.personalSign "login-1" signerAddress "Sign in to MyDapp")

Responses arrive on the same signed tag as EIP-712 -- use signatureDecoder.

signatureDecoder : Decoder String

Decode the signed response from the JS port.

case D.decodeValue Sign.signatureDecoder incoming of
    Ok sig ->
        -- sig : String -- the 0x-prefixed signature
    Err _ ->
        -- handle decode error
= SignIdle
| SignPending String
| Signed String String
| SignFailed String String
| SignRejected String

Signing state machine. Tracks the lifecycle of a single sign request.

  • SignIdle -- no sign request in progress
  • SignPending id -- awaiting user approval for request id
  • Signed id sig -- user approved; sig is the 0x-prefixed signature
  • SignFailed id err -- signing failed (wallet error, network error)
  • SignRejected id -- user explicitly cancelled in wallet UI
type SignMsg
= SignResponse String String
| SignError String String
| SignCancel String

Messages from the JS sign port.

Transition from SignIdle to SignPending for the given correlation id. All other states are unchanged (a sign already in flight is not replaced).

Update sign state from a port message. Terminal states never transition out.

True if the sign state is terminal (no more updates expected).

{ id : String
, message : String
, signature : String
}
-> Value

Verify an EIP-191 personal_sign signature by recovering the signer address (personal_ecRecover).

web3Cmd
    (Sign.verify
        { id = "login-verify-1"
        , message = "Sign in to MyDapp"
        , signature = sig
        }
    )

The recovered address arrives on the recovered tag -- decode it with recoveredDecoder and compare it against the address that claims to have signed. Failures arrive on the standard failed tag.

This is a standalone encoder returning the port value directly (like personalSign) rather than a new Cmd/Msg variant -- extending an exposed custom type is a MAJOR change under Elm's enforced semver, so additive functions keep this MINOR-safe.

Decoder
{ id : String
, address : String
}

Decode the recovered response from a verify request.

case D.decodeValue Sign.recoveredDecoder incoming of
    Ok { id, address } ->
        -- address : String -- the 0x-prefixed recovered signer
    Err _ ->
        -- not a recovered response

Standalone (not a SignMsg variant) for the same additive-safety reason as verify.