Placing dShare Orders with Safe Vaults
This guide shows how to place a dShare order where a safe vault funds the order, signs the permit, and submits the transaction.
How it works
- Approve Permit2 from the Safe (once per payment token, per chain).
- Create an order request with
createPermitand receive the EIP-712 permit. - Sign the permit with the Safe (ERC-1271 signature).
- Build the transaction with
createPermitTransaction. - Execute the transaction from the Safe.
- Poll the order request until it reaches a terminal state.
Permit2 approval is requiredDinari issues an ERC-2612 permit to any wallet without a Permit2 allowance, and a Safe cannot sign ERC-2612. See Step 3.
sequenceDiagram
autonumber
participant S as Safe
participant P as Partner
participant D as Dinari
participant C as Chain
S->>C: approve(Permit2) on payment token (one-time)
P->>D: createPermit
D-->>P: order_request_id, permit
P->>S: Sign permit (owners)
S-->>P: ERC-1271 signature
P->>D: createPermitTransaction
D-->>P: contract_address, data, value
P->>S: execTransaction
S->>C: Submit order
P->>D: orderRequests.retrieve
D-->>P: Status
Prerequisites
To place an order, an Entity needs:
- A valid KYC on the Entity.
- A valid Account under the Entity.
- Enough payment-token balance in the Safe to cover the order plus fees.
For this integration you also need:
-
A Dinari API Key ID and API Secret Key from
partners.dinari.com. Start in sandbox. - A deployed Safe (v1.3.0+) using the default
CompatibilityFallbackHandler. - Private keys for at least
thresholdSafe owners, stored server-side. - Native gas in the owner EOA that executes Safe transactions.
Step 1: Initialize the clients
import Dinari from "@dinari/api-sdk";
import Safe from "@safe-global/protocol-kit";
const dinariCredentials = await loadDinariCredentialsFromSecretManager();
const walletSecrets = await loadWalletSecretsFromSecretManager();
const config = await loadChainConfig();
const dinari = new Dinari({
apiKeyID: dinariCredentials.apiKeyID,
apiSecretKey: dinariCredentials.apiSecretKey,
environment: "sandbox", // defaults to "production"
});
const safe = await Safe.init({
provider: config.rpcUrl,
signer: walletSecrets.ownerPrivateKey, // a Safe owner
safeAddress: config.safeAddress,
});
if (!(await safe.isSafeDeployed())) {
throw new Error("Deploy the Safe before signing permits");
}
const safeAddress = await safe.getAddress();Step 2: Complete KYC and link the Safe (one-time)
KYC requires the user to complete a hosted flow, so it cannot be fully automated server-side.
const entity = await dinari.v2.entities.create({ name: "Jane Doe" });
const kyc = await dinari.v2.entities.kyc.createManagedCheck(entity.id);
console.log("Send the user to:", kyc.embed_url);
const account = await dinari.v2.entities.accounts.create(entity.id);
const accountId = account.id;
const nonceResp = await dinari.v2.accounts.wallet.external.getNonce(accountId, {
wallet_address: safeAddress,
});
const linkMessage = await safe.signMessage(safe.createMessage(nonceResp.message));
await dinari.v2.accounts.wallet.external.connect(accountId, {
chain_id: "eip155:421614",
nonce: nonceResp.nonce,
signature: linkMessage.encodedSignatures(),
wallet_address: safeAddress,
});
Link the Safe, not an owner
wallet_addressmust be the Safe address. Linking an owner EOA attributes orders to a wallet with no funds.
Step 3: Approve Permit2 (one-time, per token)
createpermit picks the permit type from the wallet's on-chain ERC-20 allowance to Permit2, not from the wallet type:
| Safe's allowance to Permit2 | Permit returned | Safe can sign? |
|---|---|---|
| None | ERC-2612 Permit | No |
| Present | Permit2 PermitTransferFrom | Yes |
Send the approval as a Safe transaction so the allowance belongs to the Safe:
import { encodeFunctionData, erc20Abi, maxUint256 } from "viem";
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const approveTx = await safe.createTransaction({
transactions: [
{
to: config.paymentTokenAddress,
value: "0",
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [PERMIT2, maxUint256],
}),
},
],
});
await safe.executeTransaction(await safe.signTransaction(approveTx));
Per token, per chainEach payment token on each chain needs its own approval. If the allowance is revoked or used up,
createPermitgoes back to returning ERC-2612 permits.
Step 4: Select the stock and order parameters
const stocks = await dinari.v2.marketData.stocks.list();
const stock = stocks.find((s) => s.symbol === "AAPL");
if (!stock) throw new Error("Stock not found");
const stockId = stock.id;| Field | Meaning |
|---|---|
stock_id | Dinari UUID of the dShare (from marketData.stocks.list()). |
order_side | "BUY" or "SELL". |
order_type | "MARKET" or "LIMIT". |
order_tif | Time-in-force, e.g. "DAY". |
payment_token | Payment-token address. Must be the token approved in Step 3. |
payment_token_quantity | For a market buy, the notional amount of payment token to spend. |
chain_id | CAIP-2 chain id, e.g. "eip155:421614". |
Market sells and limit orders use asset_quantity instead; limit orders also take limit_price. Fees are added on top of the notional.
Step 5: Create the order permit
const permitResponse = await dinari.v2.accounts.orderRequests.eip155.createPermit(
accountId,
{
chain_id: "eip155:421614",
order_side: "BUY",
order_type: "MARKET",
order_tif: "DAY",
stock_id: stockId,
payment_token: config.paymentTokenAddress,
payment_token_quantity: 10.0,
},
);
const orderRequestId = permitResponse.order_request_id;
const permit = permitResponse.permit; // { domain, types, primaryType, message }
if (permit.primaryType === "Permit") {
throw new Error("Got an ERC-2612 permit. Approve Permit2 from the Safe (Step 3).");
}Check primaryType before signing. Permit means ERC-2612 and will fail; PermitTransferFrom or PermitWitnessTransferFrom means Permit2.
Step 6: Sign the permit with the Safe
let signedPermit = await safe.signMessage(
safe.createMessage({
domain: permit.domain,
types: permit.types,
primaryType: permit.primaryType,
message: permit.message,
}),
);
// For threshold > 1, add each remaining owner's signature:
for (const ownerKey of walletSecrets.additionalOwnerKeys) {
const ownerSafe = await safe.connect({ signer: ownerKey });
signedPermit = await ownerSafe.signMessage(signedPermit);
}
const permitSignature = signedPermit.encodedSignatures();
UseencodedSignatures()Pass the Safe's encoded signature, not an owner's raw 65-byte signature. The Safe's
isValidSignaturerejects raw owner signatures.
Permits have a short deadline. If owner approvals take time, create a new permit (Step 5) once signers are ready.
Step 7: Build and send the transaction
const tx = await dinari.v2.accounts.orderRequests.eip155.createPermitTransaction(
accountId,
{ order_request_id: orderRequestId, permit_signature: permitSignature },
);
const orderTx = await safe.createTransaction({
transactions: [
{ to: tx.contract_address, data: tx.data, value: tx.value ?? "0" },
],
});
const result = await safe.executeTransaction(await safe.signTransaction(orderTx));
console.log("Tx hash:", result.hash);The owner EOA pays gas; the Safe is msg.sender. Do not send tx.data directly from an owner EOA.
Step 8: Poll order status
const orderRequest = await dinari.v2.accounts.orderRequests.retrieve(
orderRequestId,
{ account_id: accountId },
);
console.log("Status:", orderRequest.status);Two status concepts exist:
OrderRequestStatus— the request status (pre-brokerage), on theOrderRequest.BrokerageOrderStatus— the resultingOrder, viadinari.v2.accounts.orders.retrieve(orderId, { account_id }).
Permit2 and smart contract wallets
ERC-2612 permit() verifies signatures with ecrecover, which only works for EOAs. Permit2 checks whether the signer has code and, if so, calls isValidSignature on it (ERC-1271). A Safe can only pass the second check.
createPermit returns an ERC-2612 permit whenever the wallet has no Permit2 allowance for the payment token. A new Safe has none, so its first order fails. The API accepts the signature either way, because verification happens on-chain:
| Flow | What you see |
|---|---|
| User Sponsored | Safe transaction reverts, usually GS013 |
| Dinari Sponsored | No error returned; the order request expires |
Common errors
| Symptom | Likely Cause | Fix |
|---|---|---|
primaryType is Permit | No Permit2 allowance for this token/chain | Approve Permit2 from the Safe (Step 3) |
GS013 on order submission | ERC-2612 permit was signed | Approve Permit2, create and sign a new permit |
| Order request expires (Dinari Sponsored) | ERC-2612 permit was signed | Approve Permit2, create and sign a new permit |
isValidSignature reverts | Safe not deployed, or non-default fallback handler | Deploy the Safe; use CompatibilityFallbackHandler |
| Signature rejected with a Permit2 permit | Raw owner signature sent | Send encodedSignatures() |
| Transaction reverts as expired | Permit deadline passed during signing | Create a new permit |
| Wallet linking fails | Safe not deployed, or owner EOA linked | Deploy, then link the Safe address |
| Order reverts or attributed to the wrong wallet | tx.data sent from an owner EOA | Execute through the Safe (Step 7) |
| Insufficient funds | Safe balance below notional + fees | Fund the Safe; see Partner Fees |
Updated about 2 hours ago
