import {z} from 'zod' export type PolicyType = 'TRANSFER' | 'STAKE' | 'CONTRACT_CALL' | 'TYPED_MESSAGE' | 'APPROVE' | 'MINT' | 'BURN' | 'RAW' | 'COMPLIANCE' | 'DEPLOYMENT' | 'PROGRAM_CALL' | 'DAPP_CONNECTION' | 'UPGRADE' | 'ORDER' | 'AML_CHAINALYSIS_V2_SCREENING' | 'AML_CHAINALYSIS_V2_POST_SCREENING' | 'AML_ELLIPTIC_HOLISTIC_SCREENING' | 'AML_ELLIPTIC_HOLISTIC_POST_SCREENING' | 'TR_NOTABENE_SCREENING' | 'TR_NOTABENE_POST_SCREENING'; export type PolicyUserIds = Array; export type PolicyGroupIds = Array; export type PolicyOperator = 'INCLUDES'; export type InitiatorConfig = {users?: PolicyUserIds | undefined, groups?: PolicyGroupIds | undefined, services?: Array | undefined, fbAdminGroups?: Array | undefined, exchange?: Array | undefined, operator?: PolicyOperator | undefined}; export type InitiatorConfigPattern = '*' | InitiatorConfig; export type AssetTypesConfig = Array; export type AssetConfig = {nftTransfer: boolean, assetTypes?: AssetTypesConfig | undefined, operator?: PolicyOperator | undefined}; export type AccountType2 = 'EXCHANGE' | 'UNMANAGED' | 'VAULT' | 'GLOBAL_WHITELIST' | 'NETWORK_CONNECTION' | 'FIAT_ACCOUNT' | 'CONNECTED_ACCOUNT' | 'ONE_TIME_ADDRESS' | 'SRC_DST_NA' | 'MULTIPLE_VENUES' | 'END_USER_WALLET' | 'OEC_PARTNER' | 'CONTRACT' | 'INTERNAL' | 'EXTERNAL' | 'DAPP_CONNECTION' | 'UNKNOWN' | '*' | 'ANY'; export type AccountIdentifier = {type: AccountType2, id?: string | undefined, subType?: string | undefined, address?: string | undefined}; export type PolicyTag = {id: string}; export type SourceConfig = {type?: Array | undefined, subType?: Array | undefined, ids?: Array | undefined, tags?: Array | undefined, operator: PolicyOperator, matchFrom?: 'ACCOUNT' | undefined}; export type DestinationConfig = {type?: Array | undefined, subType?: Array | undefined, ids?: Array | undefined, tags?: Array | undefined, operator: PolicyOperator, matchFrom?: ('ACCOUNT' | 'SOURCE') | undefined, addressType: '*' | 'WHITELISTED' | 'ONE_TIME'}; export type AccountConfig = {type?: Array | undefined, subType?: Array | undefined, ids?: Array | undefined, tags?: Array | undefined, operator: PolicyOperator, matchFrom?: ('ACCOUNT' | 'SOURCE') | undefined}; export type OrderSide = 'BUY' | 'SELL' | 'ANY'; export type ApproversConfig = {canInitiatorApprove: boolean, operator?: ('AND' | 'OR') | undefined, allowOperatorAsAuthorizer?: boolean | undefined, approvalGroups?: (Array<{threshold: number, users?: PolicyUserIds | undefined, groups?: PolicyGroupIds | undefined}>) | undefined}; export type DesignatedSignersConfig = {type: 'SINGLE' | 'MULTIPLE' | 'INITIATOR', users?: PolicyUserIds | undefined, groups?: PolicyGroupIds | undefined}; export type VerdictConfig = {action: 'ALLOW' | 'BLOCK' | '2-TIER' | 'ACCEPT' | 'REJECT' | 'ALERT' | 'SCREEN' | 'PASS' | 'WAIT' | 'FREEZE' | 'CANCEL', approvers?: ApproversConfig | undefined, designatedSigners?: DesignatedSignersConfig | undefined}; export type AmountRangeMinMax2 = {min: string, max?: string | undefined}; export type PolicyCurrency = 'NATIVE' | 'USD' | 'EUR'; export type TimePeriodMatchType = 'PER_SINGLE_MATCH' | 'ACROSS_ALL_MATCHES'; export type TimePeriodConfig = {seconds: string, initiator: TimePeriodMatchType, source: TimePeriodMatchType, destination: TimePeriodMatchType}; export type AmountOverTimeConfig = {range: AmountRangeMinMax2, currency: PolicyCurrency, timePeriod: TimePeriodConfig}; export type AmountConfig = {range: AmountRangeMinMax2, currency?: PolicyCurrency | undefined}; export type ContractMethodConfig = {methodCalls: Array, operator: 'EITHER' | 'EXACT', payloadSuffix?: string | undefined}; export type ContractMethodPattern = '*' | ContractMethodConfig; export type ProgramCallConfig = {allowedSolanaProgramCalls: '*' | 'WHITELISTED'}; export type ScreeningMetadataConfig = {direction?: ('INBOUND' | 'OUTBOUND' | 'ANY') | undefined, provider?: ('CHAINALYSIS' | 'CHAINALYSIS_V2' | 'ELLIPTIC' | 'ELLIPTIC_HOLISTIC' | 'NOTABENE' | null) | undefined, riskRating?: ('LOW' | 'MEDIUM' | 'HIGH' | 'SEVERE' | 'ANY') | undefined, riskScore?: string | undefined, exposureType?: ('DIRECT' | 'INDIRECT' | 'ANY') | undefined, category?: Array | undefined, name?: Array | undefined, categoryId?: Array | undefined, status?: ('COMPLETED' | 'PENDING' | 'REJECTED' | 'FAILED' | 'CANCELED' | 'BLOCKING_TIME_EXPIRED') | undefined, sourceAddress?: string | undefined, destAddress?: string | undefined}; export type AmountRange = {range: AmountRangeMinMax2}; export type DAppAddressConfig = {globalWhitelisted: Array, tenantWhitelisted: Array, urls: Array, operator: PolicyOperator}; export type DerivationPathConfig = {path: Array, partial?: boolean | undefined}; export type PolicyRule = {name: string, id: string, policyEngineVersion: 'v2', type: PolicyType, subType?: PolicyType | undefined, initiator: InitiatorConfigPattern, asset?: AssetConfig | undefined, source: SourceConfig, destination?: DestinationConfig | undefined, account?: AccountConfig | undefined, side?: OrderSide | undefined, verdict: VerdictConfig, amountOverTime?: AmountOverTimeConfig | undefined, amount?: AmountConfig | undefined, externalDescriptor?: string | undefined, method?: ContractMethodPattern | undefined, isGlobalPolicy?: boolean | undefined, programCall?: ProgramCallConfig | undefined, screeningMetadata?: ScreeningMetadataConfig | undefined, quoteAsset?: AssetConfig | undefined, baseAsset?: AssetConfig | undefined, quoteAmount?: AmountRange | undefined, baseAmount?: AmountRange | undefined, dAppAddress?: DAppAddressConfig | undefined, derivationPath?: DerivationPathConfig | undefined, index?: number | undefined}; export type UpdateDraftRequest = {policyTypes: Array, rules: Array}; export type UpdateApiPolicyDraftArgs = {'Idempotency-Key'?: string | undefined, body: UpdateDraftRequest}; export type UpdateApiPolicyDraftOptions = {baseUrl?: string | undefined, fetch?: typeof fetch | undefined}; export const policyType = z.enum(["TRANSFER", "STAKE", "CONTRACT_CALL", "TYPED_MESSAGE", "APPROVE", "MINT", "BURN", "RAW", "COMPLIANCE", "DEPLOYMENT", "PROGRAM_CALL", "DAPP_CONNECTION", "UPGRADE", "ORDER", "AML_CHAINALYSIS_V2_SCREENING", "AML_CHAINALYSIS_V2_POST_SCREENING", "AML_ELLIPTIC_HOLISTIC_SCREENING", "AML_ELLIPTIC_HOLISTIC_POST_SCREENING", "TR_NOTABENE_SCREENING", "TR_NOTABENE_POST_SCREENING"]); export const policyUserIds = z.array(z.string()); export const policyGroupIds = z.array(z.string()); export const policyOperator = z.literal("INCLUDES"); export const initiatorConfig = z.object({users: policyUserIds.optional(), groups: policyGroupIds.optional(), services: z.array(z.string()).optional(), fbAdminGroups: z.array(z.string()).optional(), exchange: z.array(z.string()).optional(), operator: policyOperator.optional()}); export const initiatorConfigPattern = z.union([z.literal("*"), initiatorConfig]); export const assetTypesConfig = z.array(z.union([z.string(), z.literal("*")])); export const assetConfig = z.object({nftTransfer: z.boolean(), assetTypes: assetTypesConfig.optional(), operator: policyOperator.optional()}); export const accountType2 = z.enum(["EXCHANGE", "UNMANAGED", "VAULT", "GLOBAL_WHITELIST", "NETWORK_CONNECTION", "FIAT_ACCOUNT", "CONNECTED_ACCOUNT", "ONE_TIME_ADDRESS", "SRC_DST_NA", "MULTIPLE_VENUES", "END_USER_WALLET", "OEC_PARTNER", "CONTRACT", "INTERNAL", "EXTERNAL", "DAPP_CONNECTION", "UNKNOWN", "*", "ANY"]); export const accountIdentifier = z.object({type: accountType2, id: z.string().optional(), subType: z.string().optional(), address: z.string().optional()}); export const policyTag = z.object({id: z.string()}); export const sourceConfig = z.object({type: z.array(accountType2).optional(), subType: z.array(accountIdentifier).optional(), ids: z.array(accountIdentifier).optional(), tags: z.array(policyTag).optional(), operator: policyOperator, matchFrom: z.literal("ACCOUNT").optional()}); export const destinationConfig = z.object({type: z.array(accountType2).optional(), subType: z.array(accountIdentifier).optional(), ids: z.array(accountIdentifier).optional(), tags: z.array(policyTag).optional(), operator: policyOperator, matchFrom: z.enum(["ACCOUNT", "SOURCE"]).optional(), addressType: z.enum(["*", "WHITELISTED", "ONE_TIME"])}); export const accountConfig = z.object({type: z.array(accountType2).optional(), subType: z.array(accountIdentifier).optional(), ids: z.array(accountIdentifier).optional(), tags: z.array(policyTag).optional(), operator: policyOperator, matchFrom: z.enum(["ACCOUNT", "SOURCE"]).optional()}); export const orderSide = z.enum(["BUY", "SELL", "ANY"]); export const approversConfig = z.object({canInitiatorApprove: z.boolean(), operator: z.enum(["AND", "OR"]).optional(), allowOperatorAsAuthorizer: z.boolean().optional(), approvalGroups: z.array(z.object({threshold: z.number(), users: policyUserIds.optional(), groups: policyGroupIds.optional()})).optional()}); export const designatedSignersConfig = z.object({type: z.enum(["SINGLE", "MULTIPLE", "INITIATOR"]), users: policyUserIds.optional(), groups: policyGroupIds.optional()}); export const verdictConfig = z.object({action: z.enum(["ALLOW", "BLOCK", "2-TIER", "ACCEPT", "REJECT", "ALERT", "SCREEN", "PASS", "WAIT", "FREEZE", "CANCEL"]), approvers: approversConfig.optional(), designatedSigners: designatedSignersConfig.optional()}); export const amountRangeMinMax2 = z.object({min: z.string(), max: z.string().optional()}); export const policyCurrency = z.enum(["NATIVE", "USD", "EUR"]); export const timePeriodMatchType = z.enum(["PER_SINGLE_MATCH", "ACROSS_ALL_MATCHES"]); export const timePeriodConfig = z.object({seconds: z.string(), initiator: timePeriodMatchType, source: timePeriodMatchType, destination: timePeriodMatchType}); export const amountOverTimeConfig = z.object({range: amountRangeMinMax2, currency: policyCurrency, timePeriod: timePeriodConfig}); export const amountConfig = z.object({range: amountRangeMinMax2, currency: policyCurrency.optional()}); export const contractMethodConfig = z.object({methodCalls: z.array(z.string()), operator: z.enum(["EITHER", "EXACT"]), payloadSuffix: z.string().optional()}); export const contractMethodPattern = z.union([z.literal("*"), contractMethodConfig]); export const programCallConfig = z.object({allowedSolanaProgramCalls: z.enum(["*", "WHITELISTED"])}); export const screeningMetadataConfig = z.object({direction: z.enum(["INBOUND", "OUTBOUND", "ANY"]).optional(), provider: z.enum(["CHAINALYSIS", "CHAINALYSIS_V2", "ELLIPTIC", "ELLIPTIC_HOLISTIC", "NOTABENE"]).nullable().optional(), riskRating: z.enum(["LOW", "MEDIUM", "HIGH", "SEVERE", "ANY"]).optional(), riskScore: z.string().optional(), exposureType: z.enum(["DIRECT", "INDIRECT", "ANY"]).optional(), category: z.array(z.string()).optional(), name: z.array(z.string()).optional(), categoryId: z.array(z.string()).optional(), status: z.enum(["COMPLETED", "PENDING", "REJECTED", "FAILED", "CANCELED", "BLOCKING_TIME_EXPIRED"]).optional(), sourceAddress: z.string().optional(), destAddress: z.string().optional()}); export const amountRange = z.object({range: amountRangeMinMax2}); export const dAppAddressConfig = z.object({globalWhitelisted: z.array(z.string()), tenantWhitelisted: z.array(z.string()), urls: z.array(z.string()), operator: policyOperator}); export const derivationPathConfig = z.object({path: z.array(z.number()), partial: z.boolean().optional()}); export const policyRule = z.object({name: z.string(), id: z.string(), policyEngineVersion: z.literal("v2"), type: policyType, subType: policyType.optional(), initiator: initiatorConfigPattern, asset: assetConfig.optional(), source: sourceConfig, destination: destinationConfig.optional(), account: accountConfig.optional(), side: orderSide.optional(), verdict: verdictConfig, amountOverTime: amountOverTimeConfig.optional(), amount: amountConfig.optional(), externalDescriptor: z.string().optional(), method: contractMethodPattern.optional(), isGlobalPolicy: z.boolean().optional(), programCall: programCallConfig.optional(), screeningMetadata: screeningMetadataConfig.optional(), quoteAsset: assetConfig.optional(), baseAsset: assetConfig.optional(), quoteAmount: amountRange.optional(), baseAmount: amountRange.optional(), dAppAddress: dAppAddressConfig.optional(), derivationPath: derivationPathConfig.optional(), index: z.number().optional()}); export const policyMetadataEntry = z.object({editedBy: z.string().optional(), editedAt: z.string().optional(), publishedBy: z.string().optional(), publishedAt: z.string().optional()}); export const policyMetadata = z.record(z.string(), policyMetadataEntry); export const draftResponse = z.object({status: z.string(), rules: z.array(policyRule), draftId: z.string(), metadata: policyMetadata}); export const policyRuleError = z.object({errorMessage: z.string(), errorCode: z.number(), errorCodeName: z.string(), errorField: z.enum(["operator", "operators", "authorizationGroups", "designatedSigner", "designatedSigners", "contractMethods", "amountAggregation", "src", "dst"])}); export const policyRuleCheckResult = z.object({index: z.number(), status: z.enum(["ok", "failure"]), errors: z.array(policyRuleError)}); export const policyCheckResult = z.object({errors: z.number(), results: z.array(policyRuleCheckResult)}); export const policyValidation = z.object({status: z.string(), checkResult: policyCheckResult}); export const draftReviewAndValidationResponse = z.object({draftResponse: draftResponse, validation: policyValidation}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Update the draft with a new set of rules by policy types * * PUT /policy/draft */ export const updateApiPolicyDraft = async (args: UpdateApiPolicyDraftArgs, options?: UpdateApiPolicyDraftOptions) => { const {baseUrl = "https://api.fireblocks.io/v1", fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('content-type', "application/json") headers.set('accept', "application/json") if (args['Idempotency-Key'] != null) headers.set("Idempotency-Key", String(args['Idempotency-Key'])) const url = `${baseUrl.replace(/\/+$/, '')}/policy/draft${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "PUT", headers, body: JSON.stringify(args.body)}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(draftReviewAndValidationResponse, await res.json()) };