Skip to content

Commit

Permalink
example for approvals per spending limits
Browse files Browse the repository at this point in the history
  • Loading branch information
Ptroger committed Aug 26, 2024
1 parent db91a98 commit fc44db8
Show file tree
Hide file tree
Showing 12 changed files with 1,746 additions and 1 deletion.
12 changes: 12 additions & 0 deletions examples/approvals-by-spending-limit/.env.default
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
ADMIN_USER_ADDR=0xaaa...
MEMBER_USER_ADDR=-0xbbb...
SYSTEM_MANAGER_KEY=0xddd...
MEMBER_USER_CRED=0xeee...

AUTH_HOST=https://auth.armory.narval.xyz
AUTH_CLIENT_ID=narval-example
VAULT_HOST=https://vault.armory.narval.xyz
VAULT_CLIENT_ID=narval-example

AUTH_API_KEY=armory-admin-api-key
VAULT_API_KEY=vault-admin-api-key
1 change: 1 addition & 0 deletions examples/approvals-by-spending-limit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Collect approval before authorizing a request
64 changes: 64 additions & 0 deletions examples/approvals-by-spending-limit/approve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AuthClient, AuthConfig, buildSignerEip191, privateKeyToJwk } from "@narval/armory-sdk"
import { hexSchema } from "@narval/policy-engine-shared"
import minimist from "minimist"
import 'dotenv/config'

const main = async () => {
console.log('Starting...')

const args = minimist(process.argv.slice(2))

const userType = args.user
const authId = args._[0]

// Check if userType is provided
if (!userType) {
console.error('Please specify the user type: --user=member or --user=admin')
process.exit(1)
}

// Check if authId is provided
if (!authId) {
console.error('Please provide the authId as the second argument')
process.exit(1)
}

const CRED =
userType === 'admin' ? hexSchema.parse(process.env.ADMIN_USER_CRED) : hexSchema.parse(process.env.MEMBER_USER_CRED)

const AUTH_HOST = process.env.AUTH_HOST
const AUTH_CLIENT_ID = process.env.AUTH_CLIENT_ID

if (
!AUTH_HOST ||
!AUTH_CLIENT_ID
) {
console.error('Missing environment variables')
return
}

const authJwk = privateKeyToJwk(CRED)

const authConfig: AuthConfig = {
host: AUTH_HOST,
clientId: AUTH_CLIENT_ID,
signer: {
jwk: authJwk,
alg: 'EIP191',
sign: buildSignerEip191(CRED)
}
}

const auth = new AuthClient(authConfig)

const authRequest = await auth.getAuthorizationById(authId)
console.log('### authRequestBeforeApproval', JSON.stringify(authRequest, null, 2))

await auth.approve(authId)

const approvedAuthorizationRequest = await auth.getAuthorizationById(authId)
console.log('### approvedAuthorizationRequest', JSON.stringify(approvedAuthorizationRequest, null, 2))
console.log('Done')
}

main().catch(console.error)
28 changes: 28 additions & 0 deletions examples/approvals-by-spending-limit/armory.account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
AuthClient,
AuthConfig,
DataStoreConfig,
EntityStoreClient,
PolicyStoreClient,
VaultClient,
VaultConfig
} from '@narval-xyz/armory-sdk'

export const armoryClient = (configs: {
auth: AuthConfig
vault: VaultConfig
entityStore: DataStoreConfig
policyStore: DataStoreConfig
}) => {
const authClient = new AuthClient(configs.auth)
const vaultClient = new VaultClient(configs.vault)
const entityStoreClient = new EntityStoreClient(configs.entityStore)
const policyStoreClient = new PolicyStoreClient(configs.policyStore)

return {
authClient,
vaultClient,
entityStoreClient,
policyStoreClient
}
}
231 changes: 231 additions & 0 deletions examples/approvals-by-spending-limit/armory.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import {
AuthClient,
EntityStoreClient,
Permission,
PolicyStoreClient,
PublicKey,
VaultClient
} from '@narval-xyz/armory-sdk'
import { AccountEntity, CredentialEntity } from '@narval-xyz/armory-sdk/policy-engine-shared'
import { addressToKid, publicKeySchema } from '@narval-xyz/armory-sdk/signature'
import { Action, Criterion, EntityType, Policy, Then, UserEntity, UserRole, ValueOperators } from '@narval/policy-engine-shared'
import { Curves, KeyTypes, SigningAlg, jwkEoaSchema, privateKeyToJwk } from '@narval/signature'
import { v4 } from 'uuid'
import { Hex } from 'viem'

const setPolicies = async (policyStoreClient: PolicyStoreClient) => {
const policies: Policy[] = [
{
id: v4(),
description: 'Allows admin to do anything',
when: [
{
criterion: Criterion.CHECK_PRINCIPAL_ROLE,
args: [UserRole.ADMIN]
}
],
then: Then.PERMIT
},
{
id: v4(),
description: 'Allows managers to read, create and import wallets',
when: [
{
criterion: Criterion.CHECK_PRINCIPAL_ROLE,
args: [UserRole.MANAGER]
},
{
criterion: Criterion.CHECK_PERMISSION,
args: ['wallet:read', 'wallet:create', 'wallet:import']
}
],
then: Then.PERMIT
},
{
id: v4(),
description: 'Allows members to read wallets',
when: [
{
criterion: Criterion.CHECK_PRINCIPAL_ROLE,
args: [UserRole.MEMBER]
},
{
criterion: Criterion.CHECK_PERMISSION,
args: ['wallet:read']
}
],
then: Then.PERMIT
},
{
id: 'members-can-transfer-1-eth-per-minute',
description: 'members can transfer 1 ETH',
when: [
{
criterion: 'checkAction',
args: ['signTransaction']
},
{
criterion: 'checkPrincipalRole',
args: ['member']
},
{
criterion: 'checkIntentType',
args: ['transferNative']
},
{
criterion: 'checkIntentToken',
args: ['eip155:1/slip44:60']
},
{
criterion: 'checkSpendingLimit',
args: {
limit: '1000000000000000000',
operator: 'lt' as ValueOperators,
timeWindow: {
type: 'rolling',
value: 30
},
filters: {
perPrincipal: true,
tokens: ['eip155:1/slip44:60']
}
}
}
],
then: 'permit'
},
{
id: 'members-can-transfer-gt-1-eth-per-minute-with-approval',
description: 'members transfers for more than 1 ETH per day requires an admin approval',
when: [
{
criterion: 'checkAction',
args: ['signTransaction']
},
{
criterion: 'checkPrincipalRole',
args: ['member']
},
{
criterion: 'checkIntentType',
args: ['transferNative']
},
{
criterion: 'checkIntentToken',
args: ['eip155:1/slip44:60']
},
{
criterion: 'checkSpendingLimit',
args: {
limit: '1000000000000000000',
operator: 'gte' as ValueOperators,
timeWindow: {
type: 'rolling',
value: 30
},
filters: {
perPrincipal: true,
tokens: ['eip155:1/slip44:60']
}
}
},
{
criterion: 'checkApprovals',
args: [
{
approvalCount: 1,
countPrincipal: false,
approvalEntityType: 'Narval::UserRole' as EntityType,
entityIds: ['admin']
}
]
}
],
then: 'permit'
}
]
await policyStoreClient.signAndPush(policies)
}

export const createPublicKey = (credInput: Hex): PublicKey => {
return credInput.length === 42
? jwkEoaSchema.parse({
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: SigningAlg.ES256K,
kid: addressToKid(credInput),
addr: credInput
})
: publicKeySchema.parse(privateKeyToJwk(credInput, 'ES256K'))
}

const setEntities = async (
entityStoreClient: EntityStoreClient,
userAndCredentials: { credential: Hex; role: UserRole; id?: string }[],
accounts: AccountEntity[]
) => {
const entitiesInput = userAndCredentials.reduce(
(acc, { credential: credInput, role, id }) => {
const user: UserEntity = {
id: id || v4(),
role
}

const publicKey = createPublicKey(credInput)

const cred: CredentialEntity = {
id: publicKey.kid,
key: publicKey,
userId: user.id
}

acc.users.push(user)
acc.credentials.push(cred)

return acc
},
{
users: [] as UserEntity[],
credentials: [] as CredentialEntity[],
accounts
}
)

await entityStoreClient.signAndPush(entitiesInput)
}

export const setInitialState = async ({
armory,
userAndCredentials
}: {
armory: {
vaultClient: VaultClient
authClient: AuthClient
entityStoreClient: EntityStoreClient
policyStoreClient: PolicyStoreClient
}
userAndCredentials: { credential: Hex; role: UserRole; id?: string }[]
}) => {
const { vaultClient, authClient, entityStoreClient, policyStoreClient } = armory

await setPolicies(policyStoreClient)
await setEntities(entityStoreClient, userAndCredentials, [])

const accessToken = await authClient.requestAccessToken({
action: Action.GRANT_PERMISSION,
resourceId: 'vault',
nonce: v4(),
permissions: [Permission.WALLET_IMPORT, Permission.WALLET_CREATE, Permission.WALLET_READ]
})

const { account } = await vaultClient.generateWallet({ accessToken })

await setEntities(entityStoreClient, userAndCredentials, [
{
id: account.id,
accountType: 'eoa',
address: account.address as Hex
}
])
return account
}
Loading

0 comments on commit fc44db8

Please sign in to comment.