diff --git a/src/api/aptos.ts b/src/api/aptos.ts index 23aa9c927..bc2a43ce0 100644 --- a/src/api/aptos.ts +++ b/src/api/aptos.ts @@ -10,6 +10,7 @@ import { Faucet } from "./faucet"; import { FungibleAsset } from "./fungibleAsset"; import { General } from "./general"; import { ANS } from "./ans"; +import { Permissions } from "./permissions"; import { Staking } from "./staking"; import { Transaction } from "./transaction"; import { Table } from "./table"; @@ -27,6 +28,7 @@ import { AptosObject } from "./object"; * * const aptos = new Aptos(); */ + export class Aptos { readonly config: AptosConfig; @@ -34,6 +36,8 @@ export class Aptos { readonly ans: ANS; + readonly permissions: Permissions; + readonly coin: Coin; readonly digitalAsset: DigitalAsset; @@ -60,6 +64,7 @@ export class Aptos { this.config = new AptosConfig(settings); this.account = new Account(this.config); this.ans = new ANS(this.config); + this.permissions = new Permissions(this.config); this.coin = new Coin(this.config); this.digitalAsset = new DigitalAsset(this.config); this.event = new Event(this.config); @@ -79,6 +84,7 @@ export class Aptos { export interface Aptos extends Account, ANS, + Permissions, Coin, DigitalAsset, Event, @@ -114,6 +120,7 @@ function applyMixin(targetClass: any, baseClass: any, baseClassProp: string) { applyMixin(Aptos, Account, "account"); applyMixin(Aptos, ANS, "ans"); +applyMixin(Aptos, Permissions, "permissions"); applyMixin(Aptos, Coin, "coin"); applyMixin(Aptos, DigitalAsset, "digitalAsset"); applyMixin(Aptos, Event, "event"); diff --git a/src/api/permissions.ts b/src/api/permissions.ts new file mode 100644 index 000000000..d1ef448c6 --- /dev/null +++ b/src/api/permissions.ts @@ -0,0 +1,123 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { getPermissions, requestPermission, revokePermissions } from "../internal/permissions"; +import { AptosConfig } from "./aptosConfig"; +import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; +import { FilteredPermissions, Permission, PermissionType, RevokePermission } from "../types/permissions"; +import { AccountAddress, Ed25519PublicKey } from "../core"; + +/** + * A class to handle all permission-related operations + */ +export class Permissions { + constructor(readonly config: AptosConfig) {} + + /** + * Retrieves the current permissions granted to a sub-account by a primary account. + * + * @example + * ```typescript + * const permissions = await aptos.permissions.getPermissions({ + * primaryAccount: alice, + * subAccount: bob + * }); + * // permissions = [{asset: "0x1::aptos_coin::AptosCoin", type: "FungibleAsset", remaining: "10"}] + * ``` + * + * @param args - The arguments for retrieving permissions. + * @param args.primaryAccount - The primary account that granted permissions. + * @param args.subAccount - The sub-account that received permissions. + * @param args.filter - Optional filter to specify the type of permissions to retrieve. + * + * @returns A promise that resolves to an array of current permissions and their remaining balances. + */ + async getPermissions(args: { + primaryAccountAddress: AccountAddress; + subAccountPublicKey: Ed25519PublicKey; + + filter?: T; + }): Promise> { + return getPermissions({ + aptosConfig: this.config, + primaryAccountAddress: args.primaryAccountAddress, + subAccountPublicKey: args.subAccountPublicKey, + filter: args.filter, + }); + } + + /** + * Requests new permissions for a sub-account from a primary account. + * + * @example + * ```typescript + * const permission = FungibleAssetPermission({ + * asset: "0x1::aptos_coin::AptosCoin", + * balance: "10", + * }); + * + * const txn = await aptos.permissions.requestPermissions({ + * primaryAccount: alice, + * permissionedAccount: bob, + * permissions: [permission] + * }); + * ``` + * + * @param args - The arguments for requesting permissions. + * @param args.primaryAccount - The primary account granting permissions. + * @param args.permissionedAccount - The sub-account receiving permissions. + * @param args.permissions - Array of permission requests (e.g., APT, GAS, NFT, or NFTC). + * @param args.expiration - Optional expiration time in seconds. + * @param args.requestsPerSecond - Optional rate limit for transactions. + * + * @returns A promise that resolves to a transaction that can be signed and submitted. + */ + async requestPermissions(args: { + primaryAccountAddress: AccountAddress; + permissionedAccountPublicKey: Ed25519PublicKey; + permissions: Permission[]; + expiration?: number; + requestsPerSecond?: number; + }): Promise { + return requestPermission({ + aptosConfig: this.config, + primaryAccountAddress: args.primaryAccountAddress, + permissionedAccountPublicKey: args.permissionedAccountPublicKey, + permissions: args.permissions, + expiration: args.expiration, + requestsPerSecond: args.requestsPerSecond, + }); + } + + /** + * Revokes existing permissions from a sub-account. + * + * @example + * ```typescript + * const txn = await aptos.permissions.revokePermission({ + * primaryAccount: alice, + * subAccount: bob, + * permissions: [RevokeFungibleAssetPermission({ asset: "0x1::aptos_coin::AptosCoin" })] + * }); + * ``` + * + * @param args - The arguments for revoking permissions. + * @param args.primaryAccount - The primary account revoking permissions. + * @param args.subAccount - The sub-account losing permissions. + * @param args.permissions - Array of permissions to revoke (e.g., APT or NFT). + * + * @returns A promise that resolves to a transaction that can be signed and submitted. + */ + async revokePermission(args: { + primaryAccountAddress: AccountAddress; + subAccountPublicKey: Ed25519PublicKey; + permissions: RevokePermission[]; + }): Promise { + return revokePermissions({ + aptosConfig: this.config, + primaryAccountAddress: args.primaryAccountAddress, + subAccountPublicKey: args.subAccountPublicKey, + permissions: args.permissions, + }); + } +} diff --git a/src/internal/permissions.ts b/src/internal/permissions.ts new file mode 100644 index 000000000..aba49fc4c --- /dev/null +++ b/src/internal/permissions.ts @@ -0,0 +1,329 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * This file contains the underlying implementations for exposed API surface in + * the {@link api/name}. By moving the methods out into a separate file, + * other namespaces and processes can access these methods without depending on the entire + * name namespace and without having a dependency cycle error. + */ + +import { BatchArgument } from "@wgb5445/aptos-intent-npm"; +import { AptosConfig } from "../api/aptosConfig"; +import { AccountAddress, Ed25519PublicKey } from "../core"; +import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; +import { MoveString } from "../bcs"; +import { AptosIntentBuilder } from "../transactions"; +import { + FilteredPermissions, + PermissionType, + MoveVMPermissionType, + RevokePermission, + Permission, + buildFungibleAssetPermission, + buildNFTPermission, +} from "../types/permissions"; +import { Transaction } from "../api/transaction"; +import { view } from "./view"; + +// functions +export async function getPermissions({ + aptosConfig, + primaryAccountAddress, + subAccountPublicKey, + filter, +}: { + aptosConfig: AptosConfig; + primaryAccountAddress: AccountAddress; + subAccountPublicKey: Ed25519PublicKey; + filter?: T; +}): Promise> { + const handle = await getHandleAddress({ aptosConfig, primaryAccountAddress, subAccountPublicKey }); + + const res = await fetch(`${aptosConfig.fullnode}/accounts/${handle}/resources`); + + type NodeDataResponse = Array<{ + type: string; + data: { + perms: { + data: Array<{ + key: { data: string; type_name: string }; + value: string; + }>; + }; + }; + }>; + + const data = (await res.json()) as NodeDataResponse; + + const permissions = data[0].data.perms.data.map((d) => { + switch (d.key.type_name) { + case MoveVMPermissionType.FungibleAsset: // can this be better? i dont rly like this + return buildFungibleAssetPermission({ + asset: d.key.data, + balance: d.value, + }); + case MoveVMPermissionType.TransferPermission: + return buildNFTPermission({ + assetAddress: d.key.data, + capabilities: { transfer: true, mutate: false }, + }); + default: + // todo throw here + throw new Error(); + } + }); + + const filtered = filter ? permissions.filter((p) => filter.includes(p.type as PermissionType)) : permissions; + return filtered as FilteredPermissions; +} + +// should it return the requested permissions? on success? and when it fails it +export async function requestPermission(args: { + aptosConfig: AptosConfig; + primaryAccountAddress: AccountAddress; + permissionedAccountPublicKey: Ed25519PublicKey; + permissions: Permission[]; + expiration?: number; + requestsPerSecond?: number; +}): Promise { + const { aptosConfig, primaryAccountAddress, permissionedAccountPublicKey, permissions } = args; + const transaction = new Transaction(aptosConfig); + + // Get or create a handle for the permissioned account + const existingHandleAddress = await getHandleAddress({ + aptosConfig, + primaryAccountAddress, + subAccountPublicKey: permissionedAccountPublicKey, + }); + + return transaction.build.batched_intents({ + sender: primaryAccountAddress, + builder: async (builder) => { + // Get the permissioned signer - either create new one or use existing + const permissionedSigner = await getPermissionedSigner(builder, { + existingHandleAddress, + permissionedAccountPublicKey, + }); + + // if nft permission has multiple capabilities, we need to add multiple txns + // For NFT permissions with multiple capabilities, split into separate transactions + + const expandedPermissions = permissions.flatMap((permission) => { + if (permission.type === PermissionType.NFT && permission.capabilities) { + const expanded = []; + if (permission.capabilities.transfer) { + expanded.push({ + ...permission, + capabilities: { transfer: true, mutate: false }, + }); + } + if (permission.capabilities.mutate) { + expanded.push({ + ...permission, + capabilities: { transfer: false, mutate: true }, + }); + } + return expanded; + } + return permission; + }); + // Grant each requested permission + await Promise.all( + expandedPermissions + .map((permission) => + grantPermission(builder, { + permissionedSigner: permissionedSigner.signer, + permission, + }), + ) + .flat(), + ); + + // If we created a new handle, finalize the setup + if (permissionedSigner.isNewHandle) { + await finalizeNewHandle(builder, { + permissionedAccountPublicKey, + handle: permissionedSigner.handle!, + }); + } + + return builder; + }, + }); +} + +export async function revokePermissions(args: { + aptosConfig: AptosConfig; + primaryAccountAddress: AccountAddress; + subAccountPublicKey: Ed25519PublicKey; + permissions: RevokePermission[]; +}): Promise { + const { aptosConfig, primaryAccountAddress, subAccountPublicKey, permissions } = args; + + const transaction = new Transaction(aptosConfig); + return transaction.build.batched_intents({ + sender: primaryAccountAddress, + builder: async (builder) => { + const signer = await builder.add_batched_calls({ + function: "0x1::permissioned_delegation::permissioned_signer_by_key", + functionArguments: [BatchArgument.new_signer(0), subAccountPublicKey.toUint8Array()], + typeArguments: [], + }); + + const permissionPromises = permissions.map((permission) => { + switch (permission.type) { + case PermissionType.FungibleAsset: { + return builder.add_batched_calls({ + function: "0x1::fungible_asset::revoke_permission", + functionArguments: [signer[0].borrow(), permission.asset], + typeArguments: [], + }); + } + // TODO: object nft revoke + case PermissionType.NFT: { + return builder.add_batched_calls({ + function: "0x1::object::revoke_permission", + functionArguments: [signer[0].borrow(), permission.assetAddress], + typeArguments: ["0x4::token::Token"], + }); + } + default: { + console.log("Not implemented"); + return Promise.resolve(); + } + } + }); + + await Promise.all(permissionPromises); + return builder; + }, + }); +} + +// helper functions +async function getPermissionedSigner( + builder: AptosIntentBuilder, + args: { + existingHandleAddress: string | null; + permissionedAccountPublicKey: Ed25519PublicKey; + }, +) { + if (args.existingHandleAddress) { + const signer = await builder.add_batched_calls({ + function: "0x1::permissioned_delegation::permissioned_signer_by_key", + functionArguments: [BatchArgument.new_signer(0), args.permissionedAccountPublicKey.toUint8Array()], + typeArguments: [], + }); + return { signer, isNewHandle: false }; + } + + // Create new handle and signer + const handle = await builder.add_batched_calls({ + function: "0x1::permissioned_signer::create_storable_permissioned_handle", + functionArguments: [BatchArgument.new_signer(0), 360], + typeArguments: [], + }); + + const signer = await builder.add_batched_calls({ + function: "0x1::permissioned_signer::signer_from_storable_permissioned", + functionArguments: [handle[0].borrow()], + typeArguments: [], + }); + + return { signer, handle, isNewHandle: true }; +} + +async function grantPermission( + builder: AptosIntentBuilder, + args: { + permissionedSigner: BatchArgument[]; + permission: Permission; + }, +) { + switch (args.permission.type) { + case PermissionType.FungibleAsset: + return builder.add_batched_calls({ + function: "0x1::fungible_asset::grant_permission", + functionArguments: [ + BatchArgument.new_signer(0), + args.permissionedSigner[0].borrow(), + args.permission.asset, // do i need to convert this to AccountAddress? .... i guess not?? + args.permission.balance, + ], + typeArguments: [], + }); + case PermissionType.NFT: { + const txn: Promise[] = []; + if (args.permission.capabilities.transfer) { + return builder.add_batched_calls({ + function: "0x1::object::grant_permission", + functionArguments: [ + BatchArgument.new_signer(0), + args.permissionedSigner[0].borrow(), + args.permission.assetAddress, + ], + typeArguments: ["0x4::token::Token"], + }); + } + if (args.permission.capabilities.mutate) { + console.log("mutate not implemented"); + throw new Error("mutate not implemented"); + } + return txn; + } + default: + console.log("Not implemented"); + throw new Error(`${args.permission.type} not implemented`); + return Promise.resolve(); + } +} + +async function finalizeNewHandle( + builder: AptosIntentBuilder, + args: { + permissionedAccountPublicKey: Ed25519PublicKey; + handle: BatchArgument[]; + }, +) { + await builder.add_batched_calls({ + function: "0x1::permissioned_delegation::add_permissioned_handle", + functionArguments: [BatchArgument.new_signer(0), args.permissionedAccountPublicKey.toUint8Array(), args.handle[0]], + typeArguments: [], + }); + + await builder.add_batched_calls({ + function: "0x1::lite_account::add_dispatchable_authentication_function", + functionArguments: [ + BatchArgument.new_signer(0), + AccountAddress.ONE, + new MoveString("permissioned_delegation"), + new MoveString("authenticate"), + ], + typeArguments: [], + }); +} + +export async function getHandleAddress({ + aptosConfig, + primaryAccountAddress, + subAccountPublicKey, +}: { + aptosConfig: AptosConfig; + primaryAccountAddress: AccountAddress; + subAccountPublicKey: Ed25519PublicKey; +}) { + try { + const [handle] = await view({ + aptosConfig, + payload: { + function: "0x1::permissioned_delegation::handle_address_by_key", + functionArguments: [primaryAccountAddress, subAccountPublicKey.toUint8Array()], + }, + }); + + return handle; + } catch { + return null; + } +} diff --git a/src/types/permissions.ts b/src/types/permissions.ts new file mode 100644 index 000000000..81da8b437 --- /dev/null +++ b/src/types/permissions.ts @@ -0,0 +1,111 @@ +/** + * Types and utilities for managing permissions in the system. + * Includes permission types for fungible assets, gas, NFTs and NFT collections, + * along with interfaces and factory functions for creating and revoking permissions. + */ + +/** + * Core permission type definitions + */ +export type Permission = FungibleAssetPermission | GasPermission | NFTPermission | NFTCollectionPermission; +export type RevokePermission = RevokeFungibleAssetPermission | RevokeNFTPermission | Permission; +export type FilteredPermissions = Array>; + +/** + * Permission handle metadata and configuration + */ +export interface PermissionHandle { + // Question: Best way to approach time or date here. Date object, Epoch time string, time in milliseconds + // The contract will have this as an epoch time in seconds as a number + expiration: number; + // Question: Should this be a field at all? Should dapps be able to raise their own rate limit? + // Question: Should this be a number (1, 10, 100, etc) or a string (low, medium, high)? + transactionsPerSecond: number; + permissions: Permission[]; +} + +/** + * Capability and permission type enums + */ +export enum NFTCapability { + transfer = "transfer", + mutate = "mutate", +} + +export enum PermissionType { + FungibleAsset = "FungibleAsset", + Gas = "Gas", + NFT = "NFT", + NFTCollection = "Collection", +} + +/** + * Permission interfaces for different asset types + */ +export interface FungibleAssetPermission { + type: PermissionType.FungibleAsset; + asset: string; + // Question: best type here?: number | string | bigint + balance: string; +} + +export interface GasPermission { + type: PermissionType.Gas; + amount: number; +} + +export interface NFTPermission { + type: PermissionType.NFT; + assetAddress: string; + capabilities: Record; +} + +export interface NFTCollectionPermission { + type: PermissionType.NFTCollection; + collectionAddress: string; + capabilities: Record; +} + +export enum NFTCollectionCapability { + transfer = "transfer", + mutate = "mutate", +} + +/** + * Revoke permission types + */ +export type RevokeFungibleAssetPermission = Pick; +export type RevokeNFTPermission = Pick; + +export enum MoveVMPermissionType { + FungibleAsset = "0x1::fungible_asset::WithdrawPermission", + TransferPermission = "0x1::object::TransferPermission", +} + +/** + * Factory functions for creating permissions + */ +export function buildFungibleAssetPermission(args: Omit): FungibleAssetPermission { + return { type: PermissionType.FungibleAsset, ...args }; +} +export function buildGasPermission(args: Omit): GasPermission { + return { type: PermissionType.Gas, ...args }; +} +export function buildNFTPermission(args: Omit): NFTPermission { + return { type: PermissionType.NFT, ...args }; +} +export function buildNFTCollectionPermission(args: Omit): NFTCollectionPermission { + return { type: PermissionType.NFTCollection, ...args }; +} + +/** + * Factory functions for creating revoke permissions + */ +export function buildRevokeFungibleAssetPermission( + args: Omit, +): RevokeFungibleAssetPermission { + return { type: PermissionType.FungibleAsset, ...args }; +} +export function buildRevokeNFTPermission(args: Omit): RevokeNFTPermission { + return { type: PermissionType.NFT, ...args }; +} diff --git a/tests/e2e/transaction/delegationPermissions.test.ts b/tests/e2e/transaction/delegationPermissions.test.ts index 0b43be42d..f772fa4e3 100644 --- a/tests/e2e/transaction/delegationPermissions.test.ts +++ b/tests/e2e/transaction/delegationPermissions.test.ts @@ -5,12 +5,9 @@ /* eslint-disable no-console */ /* eslint-disable no-await-in-loop */ - -import { BatchArgument } from "@wgb5445/aptos-intent-npm"; import { Account, SigningSchemeInput, - MoveString, Network, AccountAddress, Ed25519Account, @@ -22,6 +19,14 @@ import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; import { fundAccounts, publishTransferPackage } from "./helper"; import { AbstractedEd25519Account } from "../../../src/account/AbstractedAccount"; +import { + buildFungibleAssetPermission, + buildNFTPermission, + buildRevokeFungibleAssetPermission, + Permission, + PermissionType, + RevokePermission, +} from "../../../src/types/permissions"; const LOCAL_NET = getAptosClient(); const CUSTOM_NET = getAptosClient({ @@ -52,42 +57,61 @@ describe("transaction submission", () => { await fundAccounts(LOCAL_NET.aptos, [primaryAccount, ...receiverAccounts]); }, longTestTimeout); - test.only("Able to re-grant permissions for the same subaccount", async () => { + test("Able to re-grant permissions for the same subaccount", async () => { + // account + const APT_PERMISSION = buildFungibleAssetPermission({ + asset: AccountAddress.A.toString(), // apt address + balance: "10", + }); await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [{ type: "APT", limit: 10 }], + permissions: [APT_PERMISSION], }); - const perm1 = await getPermissions({ primaryAccount, subAccount }); + const perm1 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + filter: PermissionType.FungibleAsset, + }); expect(perm1.length).toBe(1); - expect(perm1[0].remaining).toBe("10"); + expect(perm1[0].balance).toBe("10"); + const APT_PERMISSION2 = buildFungibleAssetPermission({ + asset: AccountAddress.A.toString(), + balance: "20", + }); await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [{ type: "APT", limit: 20 }], + permissions: [APT_PERMISSION2], }); - const perm2 = await getPermissions({ primaryAccount, subAccount }); + const perm2 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + filter: PermissionType.FungibleAsset, + }); expect(perm2.length).toBe(1); - expect(perm2[0].remaining).toBe("30"); + expect(perm2[0].balance).toBe("30"); }); test("Able to grant permissions for NFTs", async () => { const nftAddress = await generateNFT({ account: primaryAccount }); - const nftAddress2 = await generateNFT({ account: primaryAccount }); + // TODO: Add this back in when Runtian is done with his refactor + // const nftAddress2 = await generateNFT({ account: primaryAccount }); await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [ - { type: "NFT", address: nftAddress }, - { type: "NFT", address: nftAddress2 }, - ], + permissions: [buildNFTPermission({ assetAddress: nftAddress, capabilities: { transfer: true, mutate: false } })], }); - const perm1 = await getPermissions({ primaryAccount, subAccount }); + const perm1 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + filter: PermissionType.NFT, + }); expect(perm1.length).toBe(1); const txn1 = await signSubmitAndWait({ @@ -113,15 +137,23 @@ describe("transaction submission", () => { }, }); + const APT_PERMISSION = buildFungibleAssetPermission({ + asset: AccountAddress.A.toString(), + balance: "10", + }); await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [{ type: "APT", limit: 10 }], + permissions: [APT_PERMISSION], }); - const perm1 = await getPermissions({ primaryAccount, subAccount }); + const perm1 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + filter: PermissionType.FungibleAsset, + }); expect(perm1.length).toBe(1); - expect(perm1[0].remaining).toBe("10"); + expect(perm1[0].balance).toBe("10"); const txn1 = await signSubmitAndWait({ sender: primaryAccount, @@ -135,17 +167,24 @@ describe("transaction submission", () => { expect(txn1.response.signature?.type).toBe("single_sender"); expect(txn1.submittedTransaction.success).toBe(true); - const perm2 = await getPermissions({ primaryAccount, subAccount }); + const perm2 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + filter: PermissionType.FungibleAsset, + }); expect(perm2.length).toBe(1); - expect(perm2[0].remaining).toBe("9"); + expect(perm2[0].balance).toBe("9"); await revokePermission({ - permissions: [{ type: "APT" }], + permissions: [APT_PERMISSION], primaryAccount, subAccount, }); - const perm3 = await getPermissions({ primaryAccount, subAccount }); + const perm3 = await aptos.getPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + }); expect(perm3.length).toBe(0); }); @@ -163,10 +202,14 @@ describe("transaction submission", () => { await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [{ type: "APT", limit: 10 }], + permissions: [buildFungibleAssetPermission({ asset: AccountAddress.A.toString(), balance: "10" })], }); - await revokePermission({ primaryAccount, subAccount, permissions: [{ type: "APT" }] }); + await revokePermission({ + primaryAccount, + subAccount, + permissions: [buildRevokeFungibleAssetPermission({ asset: AccountAddress.A.toString() })], + }); const txn1 = await signSubmitAndWait({ sender: primaryAccount, @@ -194,7 +237,7 @@ describe("transaction submission", () => { await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [{ type: "APT", limit: 10 }], + permissions: [buildFungibleAssetPermission({ asset: AccountAddress.A.toString(), balance: "10" })], }); const txn1 = await signSubmitAndWait({ @@ -224,17 +267,6 @@ describe("transaction submission", () => { }); }); -// ==================================================================== -// External API -// =================================================================== - -type GrantPermission = - | { type: "APT"; limit: number; duration?: number } - | { type: "GAS"; limit: number } - | { type: "NFT"; address: string } - | { type: "NFTC"; address: string }; - -// TODO: Refactor this for clarity. It is unclear what paths are executed for repeat requests vs new requests export async function requestPermission({ primaryAccount, permissionedAccount, @@ -242,97 +274,14 @@ export async function requestPermission({ }: { primaryAccount: SingleKeyAccount; permissionedAccount: AbstractedEd25519Account; - permissions: GrantPermission[]; + permissions: Permission[]; expiration?: number; requestsPerSecond?: number; }) { - const existingHandleAddress = await getHandleAddress({ primaryAccount, subAccount: permissionedAccount }); - let handleAddress: BatchArgument | string | null = existingHandleAddress; - let permissionedSingerHandle: BatchArgument[] | null; - let permissionedSigner: BatchArgument[] | null; - - const transaction = await aptos.transaction.build.batched_intents({ - sender: primaryAccount.accountAddress, - builder: async (builder) => { - // Create a handle if one doesn't already exist - if (!existingHandleAddress) { - permissionedSingerHandle = await builder.add_batched_calls({ - function: "0x1::permissioned_signer::create_storable_permissioned_handle", - functionArguments: [BatchArgument.new_signer(0), 360], - typeArguments: [], - }); - handleAddress = permissionedSingerHandle[0].borrow(); - - permissionedSigner = await builder.add_batched_calls({ - function: "0x1::permissioned_signer::signer_from_storable_permissioned", - functionArguments: [handleAddress], - typeArguments: [], - }); - } else { - permissionedSigner = await builder.add_batched_calls({ - function: "0x1::permissioned_delegation::permissioned_signer_by_key", - functionArguments: [BatchArgument.new_signer(0), permissionedAccount.publicKey.toUint8Array()], - typeArguments: [], - }); - } - - for (const permission of permissions) { - switch (permission.type) { - case "APT": { - await builder.add_batched_calls({ - function: "0x1::fungible_asset::grant_permission", - functionArguments: [ - BatchArgument.new_signer(0), - permissionedSigner[0].borrow(), - AccountAddress.A, - permission.limit, - ], - typeArguments: [], - }); - break; - } - case "NFT": { - await builder.add_batched_calls({ - function: "0x1::object::grant_permission", - // TODO: Need to figure out if this is token id or something else - functionArguments: [BatchArgument.new_signer(0), permissionedSigner[0].borrow(), permission.address], - // TODO: Need to figure out the type argument here - typeArguments: ["0x4::token::Token"], - }); - break; - } - - default: { - console.log("Not implemented"); - break; - } - } - } - - // If we needed to create a brand new handle, then we need to attach it to finalize it - if (permissionedSingerHandle) { - await builder.add_batched_calls({ - function: "0x1::permissioned_delegation::add_permissioned_handle", - functionArguments: [ - BatchArgument.new_signer(0), - permissionedAccount.publicKey.toUint8Array(), - permissionedSingerHandle[0], - ], - typeArguments: [], - }); - await builder.add_batched_calls({ - function: "0x1::lite_account::add_dispatchable_authentication_function", - functionArguments: [ - BatchArgument.new_signer(0), - AccountAddress.ONE, - new MoveString("permissioned_delegation"), - new MoveString("authenticate"), - ], - typeArguments: [], - }); - } - return builder; - }, + const transaction = await aptos.permissions.requestPermissions({ + primaryAccountAddress: primaryAccount.accountAddress, + permissionedAccountPublicKey: permissionedAccount.publicKey, + permissions, }); const response = await aptos.signAndSubmitTransaction({ @@ -347,8 +296,6 @@ export async function requestPermission({ return response; } -type RevokePermission = { type: "APT" } | { type: "NFT" }; - export async function revokePermission({ primaryAccount, subAccount, @@ -358,35 +305,10 @@ export async function revokePermission({ subAccount: AbstractedEd25519Account; permissions: RevokePermission[]; }) { - const transaction = await aptos.transaction.build.batched_intents({ - sender: primaryAccount.accountAddress, - builder: async (builder) => { - const signer = await builder.add_batched_calls({ - function: "0x1::permissioned_delegation::permissioned_signer_by_key", - functionArguments: [BatchArgument.new_signer(0), subAccount.publicKey.toUint8Array()], - typeArguments: [], - }); - - for (const permission of permissions) { - switch (permission.type) { - case "APT": { - await builder.add_batched_calls({ - function: "0x1::fungible_asset::revoke_permission", - functionArguments: [signer[0].borrow(), AccountAddress.A], - typeArguments: [], - }); - break; - } - - default: { - console.log("Not implemented"); - break; - } - } - } - - return builder; - }, + const transaction = await aptos.permissions.revokePermission({ + primaryAccountAddress: primaryAccount.accountAddress, + subAccountPublicKey: subAccount.publicKey, + permissions, }); const response = await aptos.signAndSubmitTransaction({ @@ -404,68 +326,6 @@ export async function revokePermission({ return response; } -interface Permission { - asset: string; - type: string; - remaining: string; -} - -export async function getPermissions({ - primaryAccount, - subAccount, -}: { - primaryAccount: SingleKeyAccount; - subAccount: AbstractedEd25519Account; -}): Promise { - const handle = await getHandleAddress({ primaryAccount, subAccount }); - - const res = await fetch(`http://127.0.0.1:8080/v1/accounts/${handle}/resources`); - - type NodeDataResponse = Array<{ - type: string; - data: { - perms: { - data: Array<{ - key: { data: string; type_name: string }; - value: string; - }>; - }; - }; - }>; - - const data = (await res.json()) as NodeDataResponse; - - return data[0].data.perms.data.map((d) => ({ - asset: d.key.data, - type: d.key.type_name, - remaining: d.value, - })); -} - -// ==================================================================== -// Internal API Helper Functions -// =================================================================== -async function getHandleAddress({ - primaryAccount, - subAccount, -}: { - primaryAccount: SingleKeyAccount; - subAccount: AbstractedEd25519Account; -}) { - try { - const [handle] = await aptos.view({ - payload: { - function: "0x1::permissioned_delegation::handle_address_by_key", - functionArguments: [primaryAccount.accountAddress, subAccount.publicKey.toUint8Array()], - }, - }); - - return handle; - } catch { - return null; - } -} - // ==================================================================== // Test Helper Functions // ===================================================================