Payroll
This usecase explains how to implement private payroll.
Actors
Section titled “Actors”In this usecase, we explain a triparty system handling private transfers for contractor payouts and payroll.
The Software that exposes the private payments to their ‘users’, which are CFOs running payroll. The Payroll SaaS has a website and manages company accounts.
The Payroll SaaS does not hold funds for their users. Instead they use a multisig to manage their funds.
The Sender is the CFO running monthly payments for their company to contractors, employees, and various vendors. Each of these has provided an Ethereum wallet address to receive their funds.
The Recipients in this usecase are contractors, employees and vendors, who need to receive USDC privately from the Sender.
Requirements
Section titled “Requirements”For this usecase we ensure the following requirements hold:
- Recipients cannot see salaries of other recipients.
- Recipients cannot see Sender funds (cold or hot wallets).
- Sender can track payments.
- Payroll SaaS can display payment success.
Effectively we do not want employees to know what their colleagues are being paid, nor how much money the company has.
Workflow
Section titled “Workflow”We’ll use Union Private Payments to implement the following protocol.
- The Sender provides Recipients to the Payment SaaS (through their UI or API).
- The Payment SaaS generates unsigned transaction.
- The Payment SaaS requests the Sender to sign the unsigned transaction.
- The Payment SaaS submits the transaction on behalf of the Sender.
- The Payment SaaS monitors progress.
- (optionally) The Payment SaaS can send a notification to the recipient/update their dashboard.
- The Payment SaaS submits the redemption transaction.
- The Recipient has access to the funds.
With this workflow, we ensure compliance and that the Sender always has a full overview of transactional data. The Payment SaaS can choose to KYC recipients depending on their regulatory need.
Here we have an example implementation of the above protocol. This abstraction use viem to initialize the clients.
import { import Domain
Domain } from "@unionlabs/payments";import { import Attestor
Attestor, import EvmWalletClient
EvmWalletClient, import Payment
Payment, import Prover
Prover,} from "@unionlabs/payments/promises";import * as import Viem
Viem from "viem";import * as import ViemAccounts
ViemAccounts from "viem/accounts";import * as import ViemChains
ViemChains from "viem/chains";
declare const const privateKey: `0x${string}`
privateKey: `0x${string}`;declare const const privateKey2: `0x${string}`
privateKey2: `0x${string}`;// Make sure to replace this with your own RPC url.declare const const BASE_RPC: string
BASE_RPC: string;declare const const ASSET_ADDRESS: Domain.Erc20Address
ASSET_ADDRESS: import Domain
Domain.type Erc20Address = `0x${string}` & Brand<"Erc20Address">
Erc20Address;declare const const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID: import Domain
Domain.type UniversalChainId = string & Brand<"UniversalChainId">
UniversalChainId;declare const const BENEFICIARY: Domain.Erc20Address
BENEFICIARY: import Domain
Domain.type Erc20Address = `0x${string}` & Brand<"Erc20Address">
Erc20Address;
/** * The wallet controlled by the sender. This could be constructed * in the UI of the SaaS product. */const const senderWallet: EvmWalletClient.EvmWalletClient
The wallet controlled by the sender. This could be constructed
in the UI of the SaaS product.
senderWallet = await import EvmWalletClient
EvmWalletClient.const fromViem: (config: { account: Viem.Account | undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; ... 31 more ...; extend: <client>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}) => Promise<...>
fromViem( import Viem
Viem.createWalletClient<Viem.HttpTransport<undefined, false>, { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}, { ...;}, []>(parameters: { ...;}): { ...;}export createWalletClient
Creates a Wallet Client with a given Transport configured for a Chain.
A Wallet Client is an interface to interact with Ethereum Account(s) and provides the ability to retrieve accounts, execute transactions, sign messages, etc. through Wallet Actions.
The Wallet Client supports signing over:
- JSON-RPC Accounts (e.g. Browser Extension Wallets, WalletConnect, etc).
- Local Accounts (e.g. private key/mnemonic wallets).
createWalletClient({ account?: `0x${string}` | { address: Viem.Address; nonceManager?: Viem.NonceManager | undefined; sign: (parameters: { hash: Viem.Hash; }) => Promise<Viem.Hex>; signAuthorization: (parameters: Viem.AuthorizationRequest) => Promise<ViemAccounts.SignAuthorizationReturnType>; signMessage: ({ message }: { message: Viem.SignableMessage; }) => Promise<Viem.Hex>; signTransaction: <serializer extends Viem.SerializeTransactionFn<Viem.TransactionSerializable> = Viem.SerializeTransactionFn<Viem.TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: { serializer?: serializer | undefined; } | undefined) => Promise<Viem.Hex>; signTypedData: <const typedData extends Viem.TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: Viem.TypedDataDefinition<typedData, primaryType>) => Promise<Viem.Hex>; publicKey: Viem.Hex; source: "privateKey"; type: "local";} | Viem.Account | undefined
The Account to use for the Client. This will be used for Actions that require an account as an argument.
account: import ViemAccounts
ViemAccounts.function privateKeyToAccount(privateKey: Viem.Hex, options?: ViemAccounts.PrivateKeyToAccountOptions): Viem.PrivateKeyAccountexport privateKeyToAccount
privateKeyToAccount(const privateKey: `0x${string}`
privateKey), chain?: Viem.Chain | { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };} | undefined
Chain for the client.
chain: import ViemChains
ViemChains.const base: { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}export base
base, transport: Viem.HttpTransport<undefined, false>
The RPC transport
transport: import Viem
Viem.http<undefined, false>(url?: string | undefined, config?: Viem.HttpTransportConfig<undefined, false> | undefined): Viem.HttpTransport<undefined, false>export http
http(const BASE_RPC: string
BASE_RPC), }),);
/** * This wallet is used on the backend of the SaaS. */const const saasWallet: EvmWalletClient.EvmWalletClient
This wallet is used on the backend of the SaaS.
saasWallet = await import EvmWalletClient
EvmWalletClient.const fromViem: (config: { account: Viem.Account | undefined; batch?: { multicall?: boolean | Viem.Prettify<Viem.MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: Viem.CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Viem.Chain | undefined; experimental_blockTag?: Viem.BlockTag | undefined; key: string; name: string; pollingInterval: number; ... 31 more ...; extend: <client>(fn: (client: Viem.Client<...>) => client) => Viem.Client<...>;}) => Promise<...>
fromViem( import Viem
Viem.createWalletClient<Viem.HttpTransport<undefined, false>, { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}, { ...;}, []>(parameters: { ...;}): { ...;}export createWalletClient
Creates a Wallet Client with a given Transport configured for a Chain.
A Wallet Client is an interface to interact with Ethereum Account(s) and provides the ability to retrieve accounts, execute transactions, sign messages, etc. through Wallet Actions.
The Wallet Client supports signing over:
- JSON-RPC Accounts (e.g. Browser Extension Wallets, WalletConnect, etc).
- Local Accounts (e.g. private key/mnemonic wallets).
createWalletClient({ account?: `0x${string}` | { address: Viem.Address; nonceManager?: Viem.NonceManager | undefined; sign: (parameters: { hash: Viem.Hash; }) => Promise<Viem.Hex>; signAuthorization: (parameters: Viem.AuthorizationRequest) => Promise<ViemAccounts.SignAuthorizationReturnType>; signMessage: ({ message }: { message: Viem.SignableMessage; }) => Promise<Viem.Hex>; signTransaction: <serializer extends Viem.SerializeTransactionFn<Viem.TransactionSerializable> = Viem.SerializeTransactionFn<Viem.TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: { serializer?: serializer | undefined; } | undefined) => Promise<Viem.Hex>; signTypedData: <const typedData extends Viem.TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: Viem.TypedDataDefinition<typedData, primaryType>) => Promise<Viem.Hex>; publicKey: Viem.Hex; source: "privateKey"; type: "local";} | Viem.Account | undefined
The Account to use for the Client. This will be used for Actions that require an account as an argument.
account: import ViemAccounts
ViemAccounts.function privateKeyToAccount(privateKey: Viem.Hex, options?: ViemAccounts.PrivateKeyToAccountOptions): Viem.PrivateKeyAccountexport privateKeyToAccount
privateKeyToAccount(const privateKey2: `0x${string}`
privateKey2), chain?: Viem.Chain | { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };} | undefined
Chain for the client.
chain: import ViemChains
ViemChains.const base: { blockExplorers: { readonly default: { readonly name: "Basescan"; readonly url: "https://basescan.org"; readonly apiUrl: "https://api.basescan.org/api"; }; }; blockTime: 2000; contracts: { readonly disputeGameFactory: { readonly 1: { readonly address: "0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e"; }; }; readonly l2OutputOracle: { readonly 1: { readonly address: "0x56315b90c40730925ec5485cf004d835058518A0"; }; }; readonly multicall3: { readonly address: "0xca11bde05977b3631167028862be2a173976ca11"; readonly blockCreated: 5022; }; readonly portal: { readonly 1: { readonly address: "0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"; readonly blockCreated: 17482143; }; }; readonly l1StandardBridge: { readonly 1: { readonly address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35"; readonly blockCreated: 17482143; }; }; readonly gasPriceOracle: { readonly address: "0x420000000000000000000000000000000000000F"; }; readonly l1Block: { readonly address: "0x4200000000000000000000000000000000000015"; }; readonly l2CrossDomainMessenger: { readonly address: "0x4200000000000000000000000000000000000007"; }; readonly l2Erc721Bridge: { readonly address: "0x4200000000000000000000000000000000000014"; }; readonly l2StandardBridge: { readonly address: "0x4200000000000000000000000000000000000010"; }; readonly l2ToL1MessagePasser: { readonly address: "0x4200000000000000000000000000000000000016"; }; }; ... 11 more ...; serializers: { readonly transaction: (transaction: ViemChains.OpStackTransactionSerializable, signature?: Viem.Signature) => `0x02${string}` | `0x01${string}` | `0x03${string}` | `0x04${string}` | Viem.TransactionSerializedLegacy | `0x7e${string}`; };}export base
base, transport: Viem.HttpTransport<undefined, false>
The RPC transport
transport: import Viem
Viem.http<undefined, false>(url?: string | undefined, config?: Viem.HttpTransportConfig<undefined, false> | undefined): Viem.HttpTransport<undefined, false>export http
http(const BASE_RPC: string
BASE_RPC), }),);
/** * This is a test API_KEY. * Feel free to use it for test integrations, but it is not suitable for production usage. */const const ATTESTOR_KEY: "6af6f8068d38ebf6666b8db98f9b8b42959ab62646764bc290e663f7abb49eea"
This is a test API_KEY.
Feel free to use it for test integrations, but it is not suitable for production usage.
ATTESTOR_KEY = "6af6f8068d38ebf6666b8db98f9b8b42959ab62646764bc290e663f7abb49eea";
// The wallet client is run on the backend of the SaaS.// const paymentClient = Client.make({// rpcUrl: new URL(BASE_RPC),// assetAddress: ASSET_ADDRESS,// chainId: 8453n,// attestorApiKey: ATTESTOR_KEY,// proverUrl: "http://localhost:50051"// });
/** * Attestation service is run on the backend of the Saas. */const const attestor: Attestor.Attestor
Attestation service is run on the backend of the Saas.
attestor = await import Attestor
Attestor.const make: (options: Parameters<(options: { readonly apiKey: string; readonly baseUrl?: string | URL | undefined;}) => Layer<Attestor, SystemError, never>>[0]) => Promise<Attestor.Attestor>
make({ apiKey: string
apiKey: const ATTESTOR_KEY: "6af6f8068d38ebf6666b8db98f9b8b42959ab62646764bc290e663f7abb49eea"
This is a test API_KEY.
Feel free to use it for test integrations, but it is not suitable for production usage.
ATTESTOR_KEY, baseUrl?: string | URL | undefined
baseUrl: "http://localhost:50051",});
/** * Prover service is run on the backend of the Saas. */const const prover: Prover.Prover
Prover service is run on the backend of the Saas.
prover = await import Prover
Prover.const make: (options: Parameters<(options: { readonly proverUrl?: string | URL | undefined;}) => Layer<Prover, SystemError, never>>[0]) => Promise<Prover.Prover>
make({ proverUrl?: string | URL | undefined
proverUrl: "http://localhost:50052",});
/** * The `paymentKey` is generated by the SaaS and optionally shared with the * **sender**. Store this securely to ensure transfers are inspectable later. */const const paymentKey: Domain.PaymentKey
The paymentKey is generated by the SaaS and optionally shared with the
sender. Store this securely to ensure transfers are inspectable later.
paymentKey = await import Payment
Payment.const generateKey: () => Promise<Domain.PaymentKey>
generateKey();
/** * Beneficiaries are provided by the **sender** ahead of time. * This is the address of the **recipient**. */const const beneficiaries: Domain.Erc20Address[]
Beneficiaries are provided by the sender ahead of time.
This is the address of the recipient.
beneficiaries = [ import Domain
Domain.const Erc20Address: Brand<in out K extends string | symbol>.Constructor(args: `0x${string}`) => Domain.Erc20Address
Constructs a branded type from a value of type A, throwing an error if
the provided A is not valid.
Erc20Address("0x123"), /* ... */];
const const depositAddress: { beneficiaries: [Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address]; zAssetAddress: Domain.ZAssetAddress; destinationChainId: string & Brand<"UniversalChainId">;}
depositAddress = await import Payment
Payment.const getDepositAddress: (options: { paymentKey: Domain.PaymentKey; beneficiaries: ReadonlyArray<Domain.Erc20Address>; destinationChainId: Domain.UniversalChainId;}) => Promise<{ beneficiaries: [Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address]; zAssetAddress: Domain.ZAssetAddress; destinationChainId: string & Brand<"UniversalChainId">;}>
getDepositAddress({ paymentKey: Domain.PaymentKey
paymentKey, beneficiaries: readonly Domain.Erc20Address[]
beneficiaries, destinationChainId: string & Brand<"UniversalChainId">
destinationChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,});
/** * The Payroll SaaS creates the deposit parameters and sends * the prepared tx to be signed and sent by the sender. */const const depositResult: readonly [{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}, ...{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]]
The Payroll SaaS creates the deposit parameters and sends
the prepared tx to be signed and sent by the sender.
depositResult = await import Payment
Payment.const prepareDeposit: (options: { sourceWalletClient: WalletClient;} & Parameters<typeof prepareDeposit>[0]) => Promise<readonly [{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}, ...{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}[]]>
prepareDeposit({ DepositOptions.depositAddress: DepositAddress
depositAddress, DepositOptions.amount: bigint
Amount to deposit (in underlying token's smallest unit)
amount: 1n, sourceWalletClient: WalletClient
sourceWalletClient: const saasWallet: EvmWalletClient.EvmWalletClient
This wallet is used on the backend of the SaaS.
saasWallet, DepositOptions.srcErc20Address: Domain.Erc20Address
srcErc20Address: const ASSET_ADDRESS: Domain.Erc20Address
ASSET_ADDRESS, DepositOptions.destinationChainId: string & Brand<"UniversalChainId">
destinationChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,});
/** * Nullifier is computed by the owner of the payment key. */const const nullifier: Domain.Nullifier
Nullifier is computed by the owner of the payment key.
nullifier = await import Payment
Payment.const getNullifier: (options: { paymentKey: Domain.PaymentKey; destinationChainId: Domain.UniversalChainId;}) => Promise<Domain.Nullifier>
getNullifier({ destinationChainId: string & Brand<"UniversalChainId">
destinationChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, paymentKey: Domain.PaymentKey
paymentKey,});
/** * Proof generation is run on the backend of the SaaS. */const const proof: Proof
Proof generation is run on the backend of the SaaS.
proof = await import Payment
Payment.const generateProof: (options: { destinationPublicClient: PublicClient.PublicClient; sourcePublicClient: PublicClient.PublicClient; publicClient: PublicClient.PublicClient; prover: Prover.Prover;} & Parameters<typeof generateProof>[0]) => Promise<Proof>
generateProof({ paymentKey: Domain.PaymentKey
paymentKey, depositAddress: DepositAddress
depositAddress, nullifier: Domain.Nullifier
nullifier, amount: bigint
amount: 1n, clientIds: number[]
clientIds: [5], beneficiary: Domain.Erc20Address
beneficiary: const BENEFICIARY: Domain.Erc20Address
BENEFICIARY, selectedClientId: number
selectedClientId: 5, srcChainId: string & Brand<"UniversalChainId">
srcChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID, srcErc20Address: Domain.Erc20Address
srcErc20Address: const ASSET_ADDRESS: Domain.Erc20Address
ASSET_ADDRESS, dstErc20Address: Domain.Erc20Address
dstErc20Address: const ASSET_ADDRESS: Domain.Erc20Address
ASSET_ADDRESS, publicClient: PublicClient.PublicClient
publicClient, sourcePublicClient: PublicClient.PublicClient
sourcePublicClient: const publicClient: PublicClient.PublicClient
publicClient, destinationPublicClient: PublicClient.PublicClient
destinationPublicClient: const publicClient: PublicClient.PublicClient
publicClient, prover: Prover.Prover
prover,});
const const attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation = await const attestor: Attestor.Attestor
Attestation service is run on the backend of the Saas.
attestor.Attestor.get: (payload: { unspendableAddress: Address; beneficiary: Address;}) => Promise<{ readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}>
get({ unspendableAddress: `0x${string}`
unspendableAddress: const depositAddress: { beneficiaries: [Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address, Domain.Erc20Address]; zAssetAddress: Domain.ZAssetAddress; destinationChainId: string & Brand<"UniversalChainId">;}
depositAddress.zAssetAddress: Domain.ZAssetAddress
zAssetAddress, beneficiary: `0x${string}`
beneficiary: const BENEFICIARY: Domain.Erc20Address
BENEFICIARY,});
/** * To finalize the payment, redeem must be called. This ensures the funds arrive * unwrapped in the account of the **recipient**. If the recipients wants to receive * private assets and unwrap themselves, this does not need to be called, but the * `paymentKey` needs to be shared with the recipient. */const const preparedRedemption: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
To finalize the payment, redeem must be called. This ensures the funds arrive
unwrapped in the account of the recipient. If the recipients wants to receive
private assets and unwrap themselves, this does not need to be called, but the
paymentKey needs to be shared with the recipient.
preparedRedemption = await import Payment
Payment.const prepareRedemption: (options: { destinationWalletClient: WalletClient;} & Parameters<typeof prepareRedemption>[0]) => Promise<{ readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}>
prepareRedemption({ proof: Proof
proof, dstErc20Address: Domain.Erc20Address
dstErc20Address: const ASSET_ADDRESS: Domain.Erc20Address
ASSET_ADDRESS, attestation: { readonly id: string; readonly hash: `0x${string}`; readonly signature: `0x${string}`; readonly attestedMessage: `0x${string}`; readonly signerAddress: `0x${string}`;}
attestation, destinationWalletClient: WalletClient
destinationWalletClient: const senderWallet: EvmWalletClient.EvmWalletClient
The wallet controlled by the sender. This could be constructed
in the UI of the SaaS product.
senderWallet, // @annotate: of the destination universalChainId: string & Brand<"UniversalChainId">
universalChainId: const BASE_UNIVERSAL_CHAIN_ID: string & Brand<"UniversalChainId">
BASE_UNIVERSAL_CHAIN_ID,});
const submittedRedemption = const saasWallet: EvmWalletClient.EvmWalletClient
This wallet is used on the backend of the SaaS.
saasWallet.WalletClient.signAndSubmit: (request: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}) => Promise<{ readonly _tag: "SubmissionEvm"; readonly hash: Domain.TxHash;}>
signAndSubmit(const preparedRedemption: { readonly _tag: "PreparedEvm"; readonly kind: "Erc20.Approve" | "Erc20.Wrap" | "ZAsset.Transfer" | "LoopbackClient.Update"; readonly universalChainId: Domain.UniversalChainId; readonly contractAddress: Domain.Erc20Address; readonly abi: Viem.Abi; readonly functionName: string; readonly args: ReadonlyArray<unknown>;}
To finalize the payment, redeem must be called. This ensures the funds arrive
unwrapped in the account of the recipient. If the recipients wants to receive
private assets and unwrap themselves, this does not need to be called, but the
paymentKey needs to be shared with the recipient.
preparedRedemption);const submittedRedemption: Promise<{ readonly _tag: "SubmissionEvm"; readonly hash: Domain.TxHash;}>
Conclusion
Section titled “Conclusion”With the above protocol, we ensure all requirements from the workflow are accomplished. Companies ensure that their employees cannot see origin of funds, or salaries being paid out to their colleagues, while regular accounting tools can provide exactly the same experience as with regular transfers. In the backend, we need to introduce 1 additional onchain transaction in the payment flow, although if contractors choose to receive private assets, this can be omitted.