Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.shodai.network/llms.txt

Use this file to discover all available pages before exploring further.

This page documents the signed authorization layer used by the onchain implementation. The EIP-712 payloads connect offchain agreement definitions and user authorization to onchain deployment and input submission. These signatures are what allow the execution engine to verify that a deployment or input was authorized by the correct party. For the conceptual architecture, see System Architecture. For the API-assisted deployment and operation flow, see Workflow. Use this page only when you need the exact low-level signing inputs for POST /api/v0/agreements/deploy-with-permit or POST /api/v0/agreements/{id}/input. For normal TypeScript integrations, prefer the TypeScript client: deployAgreementWithPermit(...) signs and submits deployment permits, and submitAgreementInputWithPermit(...) signs and submits input permits. Use this reference when you are constructing typed data directly, verifying SDK helper behavior, or debugging a signing mismatch.

Deploy signing reference

The deployment permit is signed over derived on-chain parameters, not over the raw deployment request body. If you use deployAgreementWithPermit(...), the SDK performs this derivation and signing for you.

Deploy typed data

const domain = {
  name: "AgreementFactory",
  version: "1",
  chainId,
  verifyingContract: factoryAddress,
};

const types = {
  PermitAgreementWithActions: [
    { name: "docUri", type: "string" },
    { name: "docHash", type: "bytes32" },
    { name: "initialState", type: "bytes32" },
    { name: "inputDefsHash", type: "bytes32" },
    { name: "transitionsHash", type: "bytes32" },
    { name: "initVarsHash", type: "bytes32" },
    { name: "verifiersHash", type: "bytes32" },
    { name: "actionsHash", type: "bytes32" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" }
  ]
};

const message = {
  docUri,
  docHash,
  initialState,
  inputDefsHash,
  transitionsHash,
  initVarsHash,
  verifiersHash,
  actionsHash,
  nonce,
  deadline,
};

Deploy permit nonce

Set message.nonce to the current value of AgreementFactory.nonces(signer) on the target chain before signing. Read it through a live publicClient/RPC connected to the same chain as domain.chainId and factoryAddress; do not hardcode 0. A successful deploy permit consumes that factory nonce, so deploy signatures are single-use. If the signer nonce changes for any reason, including a previous successful deploy permit, the signature becomes stale and must be regenerated.

Exact derivation rules

docUri = providedDocUri ?? `ipfs://agreement/${agreement.metadata.id}`
docHash = keccak256(stringToHex(JSON.stringify(agreement)))
initialState = keccak256(stringToHex(agreement.execution.initialize.initialState))
The remaining fields are hashes of ABI-encoded contract parameters:
inputDefsHash = keccak256(abi.encode(inputDefs))
transitionsHash = keccak256(abi.encode(transitions))
initVarsHash = keccak256(abi.encode(initVars))
verifiersHash = keccak256(abi.encode(verifiers))
actionsHash = keccak256(abi.encode(actions))
Those arrays are constructed from the authored agreement like this:
ArraySourceDerivation
inputDefsagreement.execution.inputsEach input ID becomes keccak256(stringToHex(inputId)); each field ID becomes keccak256(stringToHex(fieldName)); issuer constraints become on-chain conditions.
transitionsagreement.execution.transitionsfromState = keccak256(stringToHex(from)); toState = keccak256(stringToHex(to)); inputId = keccak256(stringToHex(transition.conditions[0].input)).
initVarsVariables referenced by agreement.execution.initialize.dataEach variable ID becomes keccak256(stringToHex(variableName)); each variable value is ABI-encoded according to its field type.
verifiersVerifier registrations supplied for deploymentEach verifier registration binds a verifier key to the verifier contract address installed during agreement initialization.
actionsagreement.execution.actionsfromState and inputId are hashed to bytes32; target, value, and data come from the resolved action call.
Critical rule when participant mappings are present:
  • Do not sign against raw caller input when participant mappings change any value that is hashed or encoded into the signed message.
  • Sign against the effective post-mapping values instead.
  • For deploy, the safe source of truth is the normalized variables object returned by POST /v0/agreements/validate for the exact deployment payload you plan to submit.
For example, if agreement.execution.initialize.data references participant-backed variables such as partyAEthAddress or partyBEthAddress, and those addresses are supplied through participants, the final mapped wallet addresses still need to be reflected in the initVars used for signing. Field type encoding for initVars follows these rules:
  • string, dateTime, signatureabi.encode(string)
  • addressabi.encode(address)
  • uint256abi.encode(uint256)
  • boolabi.encode(bool)
  • bytes32, txHashabi.encode(bytes32)

Current factory addresses

  • Linea Sepolia (chainId = 59141): 0x26Ff3AdEC23fC5778f190371B1CcCadDa74e26c8
  • Linea Mainnet (chainId = 59144): 0xB772Ea12546fd7153Bf1F5ED7266B8faB0dAD6C9
For factory and implementation contract addresses with verified source links, see Contracts.

Signing call

const signatureHex = await walletClient.signTypedData({
  account,
  domain,
  types,
  primaryType: "PermitAgreementWithActions",
  message,
});
Then split signatureHex into:
  • r = 0x...
  • s = 0x...
  • v = 27 or 28
If the agreement JSON, initValues, docUri, chain, factory address, signer nonce, or deadline changes, the signature must be regenerated. The TypeScript client uses a one-hour default permit lifetime through computeDefaultDeadlineSeconds(). Low-level signing calls still require you to pass an explicit deadline.

Input signing reference

The input permit is signed over the hashed input ID and an ABI-encoded payload. If you use submitAgreementInputWithPermit(...), the SDK performs this derivation and signing for you.

Input typed data

const domain = {
  name: "AgreementEngine",
  version: "1",
  chainId,
  verifyingContract: agreementAddress,
};

const types = {
  PermitInput: [
    { name: "inputId", type: "bytes32" },
    { name: "payload", type: "bytes" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" }
  ]
};

const message = {
  inputId,
  payload,
  nonce,
  deadline,
};

Exact derivation rules

inputIdBytes32 = keccak256(stringToHex(inputId))
payload = abi.encode(dataFields)
Where dataFields has this contract shape:
type DataField = {
  id: bytes32,
  fType: uint8,
  data: bytes,
}
Build it from the authored input schema like this:
StepDerivation
Look up the input schemaRead agreement.execution.inputs[inputId].data.
Encode each submitted fieldid = keccak256(stringToHex(fieldName)); fType = the on-chain field type mapped from the authored variable type; data = ABI-encoded field value.
Encode the arrayABI-encode the whole array as (bytes32 id, uint8 fType, bytes data)[].

Field type mapping

  • uint256UINT256
  • stringSTRING
  • addressADDRESS
  • boolBOOL
  • bytes32BYTES32
  • signatureSTRING
  • dateTimeSTRING
  • txHashBYTES32

Field value encoding rules

  • STRINGabi.encode(string)
  • ADDRESSabi.encode(address)
  • UINT256abi.encode(uint256)
  • BOOLabi.encode(bool)
  • BYTES32abi.encode(bytes32)

Effective signing pipeline

fieldId = keccak256(stringToHex(fieldName))
encodedValue = abi.encode(valueForThatFieldType)
payload = abi.encode([{ id: fieldId, fType, data: encodedValue }, ...])
message = { inputId: inputIdBytes32, payload, nonce, deadline }

Signing call

const signatureHex = await walletClient.signTypedData({
  account,
  domain,
  types,
  primaryType: "PermitInput",
  message,
});
Then split signatureHex into:
  • r = 0x...
  • s = 0x...
  • v = 27 or 28
If inputId, values, agreement schema, agreement address, signer nonce, chain, or deadline changes, the signature must be regenerated.