Format with prettier. Use screen mode for invitation import - dialog mode is broken.
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { XOTemplate, XOInvitation, XOTemplateTransactionOutput } from '@xo-cash/types';
|
||||
import type { VariableInput, SelectableUTXO } from '../types.js';
|
||||
import { useState, useCallback } from "react";
|
||||
import type {
|
||||
XOTemplate,
|
||||
XOInvitation,
|
||||
XOTemplateTransactionOutput,
|
||||
} from "@xo-cash/types";
|
||||
import type { VariableInput, SelectableUTXO } from "../types.js";
|
||||
import {
|
||||
getTransactionOutputIdentifier,
|
||||
isInvitationRequirementsComplete,
|
||||
resolveProvidedLockingBytecodeHex,
|
||||
} from '../../../../utils/invitation-flow.js';
|
||||
import type { AppService } from '../../../../services/app.js';
|
||||
} from "../../../../utils/invitation-flow.js";
|
||||
import type { AppService } from "../../../../services/app.js";
|
||||
|
||||
interface InvitationManagerDeps {
|
||||
appService: AppService;
|
||||
@@ -32,26 +36,27 @@ export function useInvitationManager(deps: InvitationManagerDeps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
/** Re-check whether all invitation requirements are satisfied. */
|
||||
const refreshRequirements = useCallback(async (
|
||||
identifier: string | null = invitationId,
|
||||
): Promise<boolean> => {
|
||||
if (!identifier || !appService) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
const refreshRequirements = useCallback(
|
||||
async (identifier: string | null = invitationId): Promise<boolean> => {
|
||||
if (!identifier || !appService) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const instance = appService.invitations.find(
|
||||
(inv: any) => inv.data.invitationIdentifier === identifier,
|
||||
);
|
||||
if (!instance) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
const instance = appService.invitations.find(
|
||||
(inv: any) => inv.data.invitationIdentifier === identifier,
|
||||
);
|
||||
if (!instance) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const complete = await isInvitationRequirementsComplete(instance);
|
||||
setRequirementsComplete(complete);
|
||||
return complete;
|
||||
}, [appService, invitationId]);
|
||||
const complete = await isInvitationRequirementsComplete(instance);
|
||||
setRequirementsComplete(complete);
|
||||
return complete;
|
||||
},
|
||||
[appService, invitationId],
|
||||
);
|
||||
|
||||
/**
|
||||
* Create an invitation, persist variable values, and add
|
||||
@@ -59,177 +64,201 @@ export function useInvitationManager(deps: InvitationManagerDeps) {
|
||||
*
|
||||
* @returns The invitation identifier on success, or null on failure.
|
||||
*/
|
||||
const createWithVariables = useCallback(async (
|
||||
templateIdentifier: string,
|
||||
actionIdentifier: string,
|
||||
roleIdentifier: string,
|
||||
template: XOTemplate,
|
||||
variables: VariableInput[],
|
||||
): Promise<string | null> => {
|
||||
if (!appService) return null;
|
||||
const createWithVariables = useCallback(
|
||||
async (
|
||||
templateIdentifier: string,
|
||||
actionIdentifier: string,
|
||||
roleIdentifier: string,
|
||||
template: XOTemplate,
|
||||
variables: VariableInput[],
|
||||
): Promise<string | null> => {
|
||||
if (!appService) return null;
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatus('Creating invitation...');
|
||||
setIsProcessing(true);
|
||||
setStatus("Creating invitation...");
|
||||
|
||||
try {
|
||||
// Create via the engine
|
||||
const xoInvitation = await appService.engine.createInvitation({
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
});
|
||||
|
||||
// Wrap and track
|
||||
const invitationInstance = await appService.createInvitation(xoInvitation);
|
||||
let inv = invitationInstance.data;
|
||||
const invId = inv.invitationIdentifier;
|
||||
setInvitationId(invId);
|
||||
|
||||
// Persist variable values
|
||||
if (variables.length > 0) {
|
||||
setStatus('Adding variables...');
|
||||
const variableData = variables.map((v) => {
|
||||
const isNumeric =
|
||||
['integer', 'number', 'satoshis'].includes(v.type) ||
|
||||
(v.hint && ['satoshis', 'amount'].includes(v.hint));
|
||||
|
||||
return {
|
||||
variableIdentifier: v.id,
|
||||
roleIdentifier,
|
||||
value: isNumeric ? BigInt(v.value || '0') : v.value,
|
||||
};
|
||||
try {
|
||||
// Create via the engine
|
||||
const xoInvitation = await appService.engine.createInvitation({
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
});
|
||||
await invitationInstance.addVariables(variableData);
|
||||
inv = invitationInstance.data;
|
||||
}
|
||||
|
||||
// Build variable values lookup for output resolution
|
||||
const variableValuesByIdentifier = variables.reduce((acc, variable) => {
|
||||
if (typeof variable.value === 'string' && variable.value.trim().length > 0) {
|
||||
acc[variable.id] = variable.value;
|
||||
// Wrap and track
|
||||
const invitationInstance =
|
||||
await appService.createInvitation(xoInvitation);
|
||||
let inv = invitationInstance.data;
|
||||
const invId = inv.invitationIdentifier;
|
||||
setInvitationId(invId);
|
||||
|
||||
// Persist variable values
|
||||
if (variables.length > 0) {
|
||||
setStatus("Adding variables...");
|
||||
const variableData = variables.map((v) => {
|
||||
const isNumeric =
|
||||
["integer", "number", "satoshis"].includes(v.type) ||
|
||||
(v.hint && ["satoshis", "amount"].includes(v.hint));
|
||||
|
||||
return {
|
||||
variableIdentifier: v.id,
|
||||
roleIdentifier,
|
||||
value: isNumeric ? BigInt(v.value || "0") : v.value,
|
||||
};
|
||||
});
|
||||
await invitationInstance.addVariables(variableData);
|
||||
inv = invitationInstance.data;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// Add template-required transaction outputs
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const transaction = act?.transaction
|
||||
? template.transactions?.[act.transaction]
|
||||
: null;
|
||||
|
||||
if (transaction?.outputs && transaction.outputs.length > 0) {
|
||||
setStatus('Adding required outputs...');
|
||||
const outputsToAdd = await Promise.all(
|
||||
transaction.outputs.map(async (output: XOTemplateTransactionOutput) => {
|
||||
const outputIdentifier = getTransactionOutputIdentifier(output);
|
||||
if (!outputIdentifier) {
|
||||
throw new Error('Invalid transaction output definition');
|
||||
// Build variable values lookup for output resolution
|
||||
const variableValuesByIdentifier = variables.reduce(
|
||||
(acc, variable) => {
|
||||
if (
|
||||
typeof variable.value === "string" &&
|
||||
variable.value.trim().length > 0
|
||||
) {
|
||||
acc[variable.id] = variable.value;
|
||||
}
|
||||
|
||||
const providedHex = resolveProvidedLockingBytecodeHex(
|
||||
template,
|
||||
outputIdentifier,
|
||||
variableValuesByIdentifier,
|
||||
);
|
||||
|
||||
const lockingBytecodeHex =
|
||||
providedHex ?? await invitationInstance.generateLockingBytecode(outputIdentifier, roleIdentifier);
|
||||
|
||||
return { outputIdentifier, lockingBytecode: lockingBytecodeHex };
|
||||
}),
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
// TODO: Clean this up. Suggestions: 1. Convert to bytes above. 2. Have addOutputs accept a hex string. 3. Have addOutputs handle lockscript generation.
|
||||
await invitationInstance.addOutputs(
|
||||
outputsToAdd.map((output) => ({
|
||||
outputIdentifier: output.outputIdentifier,
|
||||
lockingBytecode: new Uint8Array(Buffer.from(output.lockingBytecode, 'hex')),
|
||||
})),
|
||||
);
|
||||
// Add template-required transaction outputs
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const transaction = act?.transaction
|
||||
? template.transactions?.[act.transaction]
|
||||
: null;
|
||||
|
||||
inv = invitationInstance.data;
|
||||
if (transaction?.outputs && transaction.outputs.length > 0) {
|
||||
setStatus("Adding required outputs...");
|
||||
const outputsToAdd = await Promise.all(
|
||||
transaction.outputs.map(
|
||||
async (output: XOTemplateTransactionOutput) => {
|
||||
const outputIdentifier = getTransactionOutputIdentifier(output);
|
||||
if (!outputIdentifier) {
|
||||
throw new Error("Invalid transaction output definition");
|
||||
}
|
||||
|
||||
const providedHex = resolveProvidedLockingBytecodeHex(
|
||||
template,
|
||||
outputIdentifier,
|
||||
variableValuesByIdentifier,
|
||||
);
|
||||
|
||||
const lockingBytecodeHex =
|
||||
providedHex ??
|
||||
(await invitationInstance.generateLockingBytecode(
|
||||
outputIdentifier,
|
||||
roleIdentifier,
|
||||
));
|
||||
|
||||
return {
|
||||
outputIdentifier,
|
||||
lockingBytecode: lockingBytecodeHex,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Clean this up. Suggestions: 1. Convert to bytes above. 2. Have addOutputs accept a hex string. 3. Have addOutputs handle lockscript generation.
|
||||
await invitationInstance.addOutputs(
|
||||
outputsToAdd.map((output) => ({
|
||||
outputIdentifier: output.outputIdentifier,
|
||||
lockingBytecode: new Uint8Array(
|
||||
Buffer.from(output.lockingBytecode, "hex"),
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
inv = invitationInstance.data;
|
||||
}
|
||||
|
||||
setInvitation(inv);
|
||||
await refreshRequirements(invId);
|
||||
setStatus("Invitation created");
|
||||
return invId;
|
||||
} catch (error) {
|
||||
showError(
|
||||
`Failed to create invitation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
||||
setInvitation(inv);
|
||||
await refreshRequirements(invId);
|
||||
setStatus('Invitation created');
|
||||
return invId;
|
||||
} catch (error) {
|
||||
showError(
|
||||
`Failed to create invitation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [appService, showError, setStatus, refreshRequirements]);
|
||||
},
|
||||
[appService, showError, setStatus, refreshRequirements],
|
||||
);
|
||||
|
||||
/**
|
||||
* Add the selected UTXOs as inputs and a change output to the invitation.
|
||||
*
|
||||
* @returns true on success, false on failure.
|
||||
*/
|
||||
const addInputsAndOutputs = useCallback(async (
|
||||
selectedUtxos: SelectableUTXO[],
|
||||
changeAmount: bigint,
|
||||
): Promise<boolean> => {
|
||||
if (!invitationId || !appService) return false;
|
||||
const addInputsAndOutputs = useCallback(
|
||||
async (
|
||||
selectedUtxos: SelectableUTXO[],
|
||||
changeAmount: bigint,
|
||||
): Promise<boolean> => {
|
||||
if (!invitationId || !appService) return false;
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatus('Adding inputs and outputs...');
|
||||
setIsProcessing(true);
|
||||
setStatus("Adding inputs and outputs...");
|
||||
|
||||
try {
|
||||
const instance = appService.invitations.find(
|
||||
(inv: any) => inv.data.invitationIdentifier === invitationId,
|
||||
);
|
||||
if (!instance) throw new Error('Invitation not found');
|
||||
try {
|
||||
const instance = appService.invitations.find(
|
||||
(inv: any) => inv.data.invitationIdentifier === invitationId,
|
||||
);
|
||||
if (!instance) throw new Error("Invitation not found");
|
||||
|
||||
const inputs = selectedUtxos.map((utxo) => ({
|
||||
outpointTransactionHash: new Uint8Array(
|
||||
Buffer.from(utxo.outpointTransactionHash, 'hex'),
|
||||
),
|
||||
outpointIndex: utxo.outpointIndex,
|
||||
}));
|
||||
const inputs = selectedUtxos.map((utxo) => ({
|
||||
outpointTransactionHash: new Uint8Array(
|
||||
Buffer.from(utxo.outpointTransactionHash, "hex"),
|
||||
),
|
||||
outpointIndex: utxo.outpointIndex,
|
||||
}));
|
||||
|
||||
await instance.addInputs(inputs);
|
||||
await instance.addOutputs([{ valueSatoshis: changeAmount }]);
|
||||
await refreshRequirements(invitationId);
|
||||
setStatus('Inputs and outputs added');
|
||||
return true;
|
||||
} catch (error) {
|
||||
showError(
|
||||
`Failed to add inputs/outputs: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [invitationId, appService, showError, setStatus, refreshRequirements]);
|
||||
await instance.addInputs(inputs);
|
||||
await instance.addOutputs([{ valueSatoshis: changeAmount }]);
|
||||
await refreshRequirements(invitationId);
|
||||
setStatus("Inputs and outputs added");
|
||||
return true;
|
||||
} catch (error) {
|
||||
showError(
|
||||
`Failed to add inputs/outputs: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
[invitationId, appService, showError, setStatus, refreshRequirements],
|
||||
);
|
||||
|
||||
/** Sign the invitation and broadcast the transaction. */
|
||||
const signAndBroadcast = useCallback(async (): Promise<boolean> => {
|
||||
if (!invitationId || !appService) return false;
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatus('Signing invitation...');
|
||||
setStatus("Signing invitation...");
|
||||
|
||||
try {
|
||||
const instance = appService.invitations.find(
|
||||
(inv: any) => inv.data.invitationIdentifier === invitationId,
|
||||
);
|
||||
if (!instance) throw new Error('Invitation not found');
|
||||
if (!instance) throw new Error("Invitation not found");
|
||||
|
||||
const complete = await refreshRequirements(invitationId);
|
||||
if (!complete) {
|
||||
showError('Invitation requirements are not complete yet.');
|
||||
showError("Invitation requirements are not complete yet.");
|
||||
return false;
|
||||
}
|
||||
|
||||
await instance.sign();
|
||||
setStatus('Broadcasting transaction...');
|
||||
setStatus("Broadcasting transaction...");
|
||||
await instance.broadcast();
|
||||
setHasSignedAndBroadcasted(true);
|
||||
setStatus('Transaction signed and broadcasted');
|
||||
showInfo('Transaction signed and broadcasted.');
|
||||
setStatus("Transaction signed and broadcasted");
|
||||
showInfo("Transaction signed and broadcasted.");
|
||||
await refreshRequirements(invitationId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -240,7 +269,14 @@ export function useInvitationManager(deps: InvitationManagerDeps) {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [invitationId, appService, setStatus, showError, showInfo, refreshRequirements]);
|
||||
}, [
|
||||
invitationId,
|
||||
appService,
|
||||
setStatus,
|
||||
showError,
|
||||
showInfo,
|
||||
refreshRequirements,
|
||||
]);
|
||||
|
||||
return {
|
||||
invitation,
|
||||
|
||||
Reference in New Issue
Block a user