Skip to content
Open
40 changes: 40 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,38 @@
color: #fff;
}

.fee-toggle {
display: inline-flex;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}

.fee-btn {
border: none;
background: none;
padding: 6px 16px;
font-size: 13px;
font-family: var(--sans);
font-weight: 500;
cursor: pointer;
color: var(--text);
transition: background 0.15s, color 0.15s;

&:not(:last-child) {
border-right: 1px solid var(--border);
}

&:hover:not(.fee-btn--active) {
background: var(--code-bg);
}
}

.fee-btn--active {
background: var(--accent);
color: #fff;
}

.records-list {
list-style: none;
padding: 0;
Expand Down Expand Up @@ -219,6 +251,14 @@
white-space: nowrap;
}

.record-number-label {
font-weight: 700;
color: var(--text);
white-space: nowrap;
text-decoration: underline;
font-size: 14px;
}

.record-value {
font-family: var(--mono);
color: var(--text-h);
Expand Down
43 changes: 39 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function App() {
const [aleoClient, setAleoClient] = useState<AleoClient<'testnet' | 'mainnet'>>(testnetAleoClient);
const [network, setNetwork] = useState<'testnet' | 'mainnet'>('testnet');
const [joinStrategy, setJoinStrategy] = useState<joinStrategyName>("basic");
const [feePrivate, setFeePrivate] = useState<boolean>(false);
const [aleoAutoJoin, setAutoJoinClient] = useState<AutoJoinClient | undefined>();

const [privateKeyInput, setPrivateKeyInput] = useState(import.meta.env.VITE_DEFAULT_PKEY || '');
Expand Down Expand Up @@ -121,8 +122,8 @@ function App() {
setError(null);
setJoinLoading(true);
try {
const newRecord = await aleoAutoJoin.joinRecords(records);
setRecords([newRecord]);
const newRecords = await aleoAutoJoin.joinRecords(records, feePrivate);
setRecords(newRecords);
} catch (e: unknown) {
if (e instanceof Error) {
setError(`Join operation failed: ${e}`);
Expand Down Expand Up @@ -233,7 +234,7 @@ function App() {
onClick={() => {
setLoading(true);
setJoinStrategy("basic");
setAutoJoinClient(new AutoJoinClient(aleoClient, aleoAccount!, BasicAutoJoinStrategy));
setAutoJoinClient(new AutoJoinClient(aleoClient, aleoAccount!, getJoinStrategyClass("basic")));
setLoading(false);
}}
>
Expand All @@ -245,13 +246,43 @@ function App() {
onClick={() => {
setLoading(true);
setJoinStrategy("batch");
setAutoJoinClient(new AutoJoinClient(aleoClient, aleoAccount!, BatchAutoJoinStrategy));
setAutoJoinClient(new AutoJoinClient(aleoClient, aleoAccount!, getJoinStrategyClass("batch")));
setLoading(false);
}}
>
Batch
</button>
</div>
<label className="field-label">Fee</label>
<div className="fee-toggle">
<button
type="button"
className={`strategy-btn${feePrivate === false ? ' strategy-btn--active' : ''}`}
onClick={() => {
setFeePrivate(false);
}}
>
Public
</button>
<button
type="button"
className={`strategy-btn${feePrivate === true ? ' strategy-btn--active' : ''}`}
onClick={() => {
setFeePrivate(true);
}}
>
Private
</button>
</div>
{/* {feePrivate && (
<div>
<label className="field-label">Fee Record (Ciphertext)</label>
<div className="input-wrap">
<input className="form-input" placeholder='record1...' onChange={e => setPrivateFeeRecord(e.target.value)}/>
</div>
</div>
)} */}

<div className="form-group">
<label htmlFor="program-name" className="field-label">
Program Name
Expand Down Expand Up @@ -280,10 +311,14 @@ function App() {
<ul className="records-list">
{records.map((record, i) => (
<li key={i} className="record-item">
<span className="record-number-label">Record #{i+1}:</span>
<span className="record-value"/>
<span className="record-label">Amount</span>
<span className="record-value">
{record.amount === undefined ? "-" : (Number(record.amount) / 1e6).toFixed(6)}
</span>
<span className="record-label">Ciphertext</span>
<span className="record-value">{record.cipherText.toString()}</span>
<span className="record-label">Tx ID</span>
<span className="record-value">{record.transactionId}</span>
</li>
Expand Down
28 changes: 27 additions & 1 deletion src/aleo/aleoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ export class AleoClient<NetworkKey extends AleoNetwork> {
unspent: true,
filter: { programs: programNames },
});
return records.map(r => this.ownedRecordToAleoRecord(r, account));

// Only return Token (USAD/USDCx) or credits (ALEO) records, not Credentials or any other record type
return records
.filter(r => (r.record_name === "credits" || r.record_name == "Token"))
.map(r =>this.ownedRecordToAleoRecord(r, account))
.filter(r => r.amount!=="0");
}

ownedRecordToAleoRecord(ownedRecord: OwnedRecord, account: Account): AleoRecord {
Expand Down Expand Up @@ -221,6 +226,27 @@ export class AleoClient<NetworkKey extends AleoNetwork> {
});
}

/** Calls out to the delegated proving system to submit a request for proving, retrying a number of times if there's an error. **/
async submitProvingRequestwithRetries(provingRequest: ProvingRequest, retries: number, attempts?: number): Promise<ProvingResponse> {
try {
const provingResponse = await this.submitProvingRequest(provingRequest);
return provingResponse;
} catch (error) {
let num_attempts = (attempts ? attempts : 0);
if (retries > 0) {
await new Promise(res => setTimeout(res, 5000 * (num_attempts+1)));
console.log(`Retrying... (${retries} left)`);
return this.submitProvingRequestwithRetries(provingRequest, retries - 1,(num_attempts+1));
} else {
if (error instanceof Error) {
throw Error(error.message);
} else {
throw Error("An unexpected error occurred");
}
}
}
}

async submitTransaction(transaction: Transaction, waitForConfirmation: boolean = false) {
const transactionId = await this.networkClient.submitTransaction(transaction);
if (waitForConfirmation) {
Expand Down
73 changes: 70 additions & 3 deletions src/aleo/autojoin/autoJoinClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,78 @@ export class AutoJoinClient {
return this.programManager;
}

async joinRecords(records: AleoRecord[]): Promise<AleoRecord> {
async joinRecords(records: AleoRecord[], feePrivate: boolean): Promise<AleoRecord[]> {
if (records.length === 0) throw new Error('No records found');
if (records.length === 1) return records[0];
if (records.length === 1) return [records[0]];
const joinStrategy = new this.joinStrategyClass(this);
AutoJoinClient.validateRecordsForJoining(records, joinStrategy);
return await joinStrategy.joinRecords(records);
return await joinStrategy.joinRecords(records, feePrivate);
}

async splitCreditsRecord(creditsRecord: AleoRecord, amountInMicrocredits: number): Promise<[AleoRecord, AleoRecord]>{
const programManager = await this.getProgramManager();
const provingRequest = await programManager.provingRequest({
programName: "credits.aleo",
functionName: 'split',
priorityFee: 0,
privateFee: false,
inputs: [
creditsRecord.plainText.toString(),
amountInMicrocredits.toString() + "u64"
],
broadcast: true,
});
const {transaction, broadcast_result} = await this.aleoClient.submitProvingRequestwithRetries(provingRequest,3);
if (broadcast_result?.status !== "Accepted") throw new Error(`Broadcast status not accepted: ${JSON.stringify(broadcast_result)}`);
const transactionId = transaction?.id;
if (!transactionId) throw new Error(`Transaction invalid: ${transaction}`);

const firstOutput = transaction.execution?.transitions?.[0]?.outputs?.[0];
if (!firstOutput?.value) throw new Error('No output record 1 in split transaction');
let newRecord = this.aleoClient.recordCipherTextStringToAleoRecord(
firstOutput.value,
this.account,
"credits.aleo",
transaction.id.trim(),
);

const secondOutput = transaction.execution?.transitions?.[0]?.outputs?.[1];
if (!secondOutput?.value) throw new Error('No output record 2 in split transaction');
let change = this.aleoClient.recordCipherTextStringToAleoRecord(
secondOutput.value,
this.account,
"credits.aleo",
transaction.id.trim(),
);

return [newRecord, change];
}

async generateMasterFeeRecord(creditsRecords: AleoRecord[], totalCostInMicrocredits: number): Promise<[AleoRecord[],AleoRecord]> {
// Sort credits records in ascending order of value
creditsRecords.sort((r1, r2) => Number(r1.amount) - Number(r2.amount!));
for (let i: number = 0; i < creditsRecords.length; i++) {
// Find the first (smallest balance) credits record that is enough to cover the total cost of fees
if (Number(creditsRecords[i].amount) >= totalCostInMicrocredits){
let [newRecord, change] = await this.splitCreditsRecord(creditsRecords[i], totalCostInMicrocredits);
creditsRecords.splice(i,1);
creditsRecords.push(change);
return [creditsRecords, newRecord];;
}
}

throw Error("No records with large enough balance to pay for gas fees.");
}

async generateFeeRecords(creditsRecord: AleoRecord, numberOfRecordsNeeded: number, amountPerRecord: number): Promise<[AleoRecord[],AleoRecord]> {
let leftovers: AleoRecord = creditsRecord;
let feeRecords: AleoRecord[] = [];

for (let i: number = 0; i < numberOfRecordsNeeded; i++) {
let [newRecord, change] = await this.splitCreditsRecord(leftovers, amountPerRecord);
feeRecords.push(newRecord);
leftovers = change
}
return [feeRecords,leftovers];
}
}
2 changes: 1 addition & 1 deletion src/aleo/autojoin/joinStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {AutoJoinClient} from "./autoJoinClient.ts";
import type {AleoRecord} from "../aleoClient.ts";

export interface JoinStrategy {
joinRecords(records: AleoRecord[]): Promise<AleoRecord>;
joinRecords(records: AleoRecord[], feePrivate: boolean): Promise<AleoRecord[]>;
isSupportedProgram(programName: string): boolean;
}

Expand Down
Loading