Skip to content

Commit

Permalink
Transfer & Approval demo
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Aug 27, 2024
1 parent 38ad828 commit c35adc0
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 672 deletions.
22 changes: 12 additions & 10 deletions examples/approvals-by-spending-limit/.env.default
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
ADMIN_USER_ADDR=0xaaa...
MEMBER_USER_ADDR=-0xbbb...
SYSTEM_MANAGER_KEY=0xddd...
MEMBER_USER_CRED=0xeee...
# Already provisioned
AUTH_HOST=http://localhost:3005
VAULT_HOST=http://localhost:3011
CLIENT_ID=approval-example-xx
CLIENT_SECRET=... # Get this when you provision your client

AUTH_HOST=https://auth.armory.narval.xyz
AUTH_CLIENT_ID=narval-example
VAULT_HOST=https://vault.armory.narval.xyz
VAULT_CLIENT_ID=narval-example
# Datastore signing key set when provisioning the client, do not change this.
# This MUST match the one you set during provision
DATA_STORE_SIGNER_ADDRESS=0x000c0d191308a336356bee3813cc17f6868972c4 # This is the Root from dev.fixture.ts
DATA_STORE_SIGNER_PRIVATE_KEY=0xa95b097938cc1d1a800d2b10d2a175f979613c940868460fd66830059fc1e418

AUTH_API_KEY=armory-admin-api-key
VAULT_API_KEY=vault-admin-api-key
# User Credentials - these can be changed & they'll be used when running `setup`
ADMIN_USER_PRIVATE_KEY=0x454c9f13f6591f6482b17bdb6a671a7294500c7dd126111ce1643b03b6aeb354 # Alice from dev.fixture.ts
MEMBER_USER_PRIVATE_KEY=0x569a6614716a76fdb9cf21b842d012add85e680b51fd4fb773109a93c6c4f307 # Bob from dev.fixture.ts
48 changes: 48 additions & 0 deletions examples/approvals-by-spending-limit/1-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable no-console */
import { hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared'
import 'dotenv/config'
import { getArmoryClients } from './armory.sdk'
import { buildEntities, policies } from './data'

// Setup is going to assume we already have a Client provisioned, and we have a PK for the datastore (same key for policies & entities)
// Setup will bootstrap the initial policy & entities
// 1. Create a system-manager user with the same PK as the datastore, so it can generate new Accounts & update data itself.
// 2. Create an Admin User, from a PK in the .env
// 3. Create a Member User, from a PK in the .env
// 4. Set the policies we'll be using

const main = async () => {
console.log('🚀 Starting...\n')
const dataStoreSignerPrivateKey = hexSchema.parse(process.env.DATA_STORE_SIGNER_PRIVATE_KEY)
const adminUserPrivateKey = hexSchema.parse(process.env.ADMIN_USER_PRIVATE_KEY)
const memberUserPrivateKey = hexSchema.parse(process.env.MEMBER_USER_PRIVATE_KEY)
const vaultHost = process.env.VAULT_HOST
const authHost = process.env.AUTH_HOST
const clientId = process.env.CLIENT_ID
const clientSecret = process.env.CLIENT_SECRET

if (!authHost || !vaultHost || !clientId || !clientSecret) {
throw new Error('Missing configuration')
}
const armory = await getArmoryClients(dataStoreSignerPrivateKey, {
clientId,
clientSecret,
vaultHost,
authHost
})

console.log('🔒 Setting policies...\n')
await armory.policyStoreClient.signAndPush(policies)

console.log('🏗️ Setting initial entity data... \n')
const entities = buildEntities({
adminUserPrivateKey,
memberUserPrivateKey,
dataStoreSignerPrivateKey
})
await armory.entityStoreClient.signAndPush(entities)

console.log('✅ Setup completed successfully \n')
}

main().catch(console.error)
30 changes: 30 additions & 0 deletions examples/approvals-by-spending-limit/2-add-destination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import { AddressBookAccountEntity } from '@narval-xyz/armory-sdk/policy-engine-shared'
import 'dotenv/config'
import { uniqBy } from 'lodash/fp'
import { getArmoryClientsFromEnv } from './armory.sdk'

// Adds the below Destination account into the Address Book
const main = async () => {
console.log('🚀 Add Destination - Starting...\n')
const armory = await getArmoryClientsFromEnv()

console.log('🏗️ Adding `0x9f38879167acCf7401351027EE3f9247A71cd0c5` as an `internal` destination... \n')

const destination: AddressBookAccountEntity = {
chainId: 1,
address: '0x9f38879167acCf7401351027EE3f9247A71cd0c5', // Engineering account from dev.fixture.ts
id: 'eip155:1:0x9f38879167acCf7401351027EE3f9247A71cd0c5',
classification: 'internal'
}

const entities = await armory.entityStoreClient.fetch()
const addressBook = uniqBy('id', [...entities.data.addressBook, destination])
entities.data.addressBook = addressBook

await armory.entityStoreClient.signAndPush(entities.data)

console.log('✅ Add Destination - Complete \n')
}

main().catch(console.error)
29 changes: 29 additions & 0 deletions examples/approvals-by-spending-limit/3-add-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-console */
import { AccountEntity } from '@narval-xyz/armory-sdk/policy-engine-shared'
import 'dotenv/config'
import { uniqBy } from 'lodash/fp'
import { getArmoryClientsFromEnv } from './armory.sdk'

// Adds the below managed account into the data store
const main = async () => {
console.log('🚀 Add Account - Starting...\n')
const armory = await getArmoryClientsFromEnv()

console.log('🏗️ Adding `0x76d1b7f9b3F69C435eeF76a98A415332084A856F` as an managed account... \n')

const account: AccountEntity = {
id: 'acct-ops-account-b',
address: '0x76d1b7f9b3F69C435eeF76a98A415332084A856F', // Operations account from dev.fixture.ts
accountType: 'eoa'
}

const entities = await armory.entityStoreClient.fetch()
const accounts = uniqBy('id', [...entities.data.accounts, account])
entities.data.accounts = accounts

await armory.entityStoreClient.signAndPush(entities.data)

console.log('✅ Add Account - Complete \n')
}

main().catch(console.error)
82 changes: 82 additions & 0 deletions examples/approvals-by-spending-limit/4-transfer-a-to-b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable no-console */
import {
AuthClient,
AuthConfig,
Decision,
Request,
TransactionRequest,
buildSignerEip191,
privateKeyToJwk
} from '@narval-xyz/armory-sdk'
import { hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared'
import 'dotenv/config'
import { v4 } from 'uuid'

const transactionRequest = {
from: '0x0301e2724a40E934Cce3345928b88956901aA127', // Account A
to: '0x76d1b7f9b3F69C435eeF76a98A415332084A856F', // Account B
chainId: 1,
value: '0x429D069189E0000', // 0.3 ETH
gas: 123n,
maxFeePerGas: 789n,
maxPriorityFeePerGas: 456n,
nonce: 193
} as TransactionRequest

const nonce = v4()
const request: Request = {
action: 'signTransaction',
resourceId: 'acct-treasury-account-a', // account.id in data.ts
transactionRequest,
nonce
}

const main = async () => {
console.log(
`🚀 Transferring 0.3 ETH - from \x1b[32m${request.resourceId}\x1b[0m to \x1b[34macct-ops-account-b\x1b[0m \n`
)
const memberUserPrivateKey = hexSchema.parse(process.env.MEMBER_USER_PRIVATE_KEY)
const host = process.env.AUTH_HOST
const clientId = process.env.CLIENT_ID
if (!host || !clientId) {
throw new Error('Missing configuration')
}

const authJwk = privateKeyToJwk(memberUserPrivateKey)
const signer = buildSignerEip191(memberUserPrivateKey)
const authConfig: AuthConfig = {
host,
clientId,
signer: {
jwk: authJwk,
alg: 'EIP191',
sign: signer
}
}
const auth = new AuthClient(authConfig)

// Make the authorization request
const response = await auth.authorize(request)

switch (response.decision) {
case Decision.PERMIT: {
console.log('✅ Transaction approved \n')
console.log('🔐 Approval token: \n', response.accessToken.value)
break
}
case Decision.CONFIRM: {
console.log('🔐 Request needs approvals', { authId: response.authId }, '\n')
console.table(response.approvals.missing)
// ... existing code ...
console.log(`To approve, run:\n\n\t\x1b[1m\x1b[33mtsx 5-approve-transfer.ts ${response.authId}\x1b[0m\n`)
// ... existing code ...
break
}
case Decision.FORBID: {
console.error('❌ Unauthorized')
console.log('🔍 Response', response, '\n')
}
}
}

main().catch(console.error)
59 changes: 59 additions & 0 deletions examples/approvals-by-spending-limit/5-approve-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable no-console */
import { AuthClient, AuthConfig, Decision, buildSignerEip191, privateKeyToJwk } from '@narval-xyz/armory-sdk'
import { hexSchema } from '@narval-xyz/armory-sdk/policy-engine-shared'
import 'dotenv/config'
import minimist from 'minimist'

const main = async () => {
const args = minimist(process.argv.slice(2))
const authId = args._[0]

console.log(`\x1b[32m🚀 Approving Transfer as Admin\x1b[0m - \x1b[36m${authId}\x1b[0m \n`)

const adminUserPrivateKey = hexSchema.parse(process.env.ADMIN_USER_PRIVATE_KEY)
const host = process.env.AUTH_HOST
const clientId = process.env.CLIENT_ID
if (!host || !clientId) {
throw new Error('Missing configuration')
}

const authJwk = privateKeyToJwk(adminUserPrivateKey)
const signer = buildSignerEip191(adminUserPrivateKey)
const authConfig: AuthConfig = {
host,
clientId,
signer: {
jwk: authJwk,
alg: 'EIP191',
sign: signer
}
}
const auth = new AuthClient(authConfig)

const authRequest = await auth.getAuthorizationById(authId)
console.log('🔎 Found pending request')

await auth.approve(authId)
const approvedAuthorizationRequest = await auth.getAuthorizationById(authId)
const result = approvedAuthorizationRequest.evaluations.find(({ decision }) => decision === Decision.PERMIT)

switch (result?.decision) {
case Decision.PERMIT: {
console.log('✅ Transaction approved \n')
console.log('🔐 Approval token: \n', result.signature)
break
}
case Decision.CONFIRM: {
console.log('🔐 Request still needs approvals', { authId: approvedAuthorizationRequest.id }, '\n')
console.table(result.approvalRequirements?.missing)
break
}
case Decision.FORBID:
default: {
console.error('❌ Unauthorized')
console.log('🔍 Response', result, '\n')
}
}
}

main().catch(console.error)
28 changes: 28 additions & 0 deletions examples/approvals-by-spending-limit/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
# Collect approval before authorizing a request
Setup

`npm i` (run from this directory)

Run each command in order

1. Set up Policies & base Entity data (see data.ts) - as the system manager

`tsx 1-setup.ts`

2. Add a new Destination to the address book - as the system manager

`tsx 2-add-destination.ts`

3. Add a second Account to be managed - as the system manager

`tsx 3-add-account.ts`

4. Transfer from Account A to Account B - as the Member User

Run the transfer 4 times in a row; the 4th requires an Approval

`tsx 4-transfer-a-to-b.ts`

5. Use the ID from the pending request to approve - as the Admin User

`tsx 5-approve-transfer.ts $REQUEST_ID`

61 changes: 0 additions & 61 deletions examples/approvals-by-spending-limit/approve.ts

This file was deleted.

28 changes: 0 additions & 28 deletions examples/approvals-by-spending-limit/armory.account.ts

This file was deleted.

Loading

0 comments on commit c35adc0

Please sign in to comment.