Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
712 changes: 285 additions & 427 deletions README.md

Large diffs are not rendered by default.

63 changes: 0 additions & 63 deletions src/OfflineStateTransitionClient.ts

This file was deleted.

58 changes: 26 additions & 32 deletions src/StateTransitionClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Authenticator } from '@unicitylabs/commons/lib/api/Authenticator.js';
import { InclusionProof, InclusionProofVerificationStatus } from '@unicitylabs/commons/lib/api/InclusionProof.js';
import { RequestId } from '@unicitylabs/commons/lib/api/RequestId.js';
import { SubmitCommitmentStatus } from '@unicitylabs/commons/lib/api/SubmitCommitmentResponse.js';
import {
SubmitCommitmentResponse,
SubmitCommitmentStatus,
} from '@unicitylabs/commons/lib/api/SubmitCommitmentResponse.js';
import { HashAlgorithm } from '@unicitylabs/commons/lib/hash/HashAlgorithm.js';
import { SigningService } from '@unicitylabs/commons/lib/signing/SigningService.js';
import { HexConverter } from '@unicitylabs/commons/lib/util/HexConverter.js';
Expand Down Expand Up @@ -57,17 +59,28 @@ export class StateTransitionClient {
public async submitMintTransaction<T extends MintTransactionData<ISerializable | null>>(
transactionData: T,
): Promise<Commitment<T>> {
return this.sendTransaction(
const commitment = await Commitment.create(
transactionData,
await SigningService.createFromSecret(MINTER_SECRET, transactionData.tokenId.bytes),
);

const result = await this.client.submitTransaction(
commitment.requestId,
commitment.transactionData.hash,
commitment.authenticator,
);

if (result.status !== SubmitCommitmentStatus.SUCCESS) {
throw new Error(`Could not submit transaction: ${result.status}`);
}

return commitment;
}

/**
* Submit a state transition for an existing token.
*
* @param transactionData Data describing the transition
* @param signingService Signing service for the current owner
* @param {Commitment} commitment Commitment containing the request information
* @returns Commitment ready for inclusion proof retrieval
* @throws Error if ownership verification fails or aggregator rejects
*
Expand All @@ -76,15 +89,16 @@ export class StateTransitionClient {
* const commitment = await client.submitTransaction(data, signingService);
* ```
*/
public async submitTransaction(
transactionData: TransactionData,
signingService: SigningService,
): Promise<Commitment<TransactionData>> {
if (!(await transactionData.sourceState.unlockPredicate.isOwner(signingService.publicKey))) {
throw new Error('Failed to unlock token');
public submitCommitment(commitment: Commitment<TransactionData>): Promise<SubmitCommitmentResponse> {
if (!commitment.transactionData.sourceState.unlockPredicate.isOwner(commitment.authenticator.publicKey)) {
throw new Error('Ownership verification failed: Authenticator does not match source state predicate.');
}

return this.sendTransaction(transactionData, signingService);
return this.client.submitTransaction(
commitment.requestId,
commitment.transactionData.hash,
commitment.authenticator,
);
}

/**
Expand Down Expand Up @@ -196,24 +210,4 @@ export class StateTransitionClient {
): Promise<InclusionProof> {
return this.client.getInclusionProof(commitment.requestId);
}

private async sendTransaction<TD extends TransactionData | MintTransactionData<ISerializable | null>>(
transactionData: TD,
signingService: SigningService,
): Promise<Commitment<TD>> {
const requestId = await RequestId.create(signingService.publicKey, transactionData.sourceState.hash);

const authenticator = await Authenticator.create(
signingService,
transactionData.hash,
transactionData.sourceState.hash,
);
const result = await this.client.submitTransaction(requestId, transactionData.hash, authenticator);

if (result.status !== SubmitCommitmentStatus.SUCCESS) {
throw new Error(`Could not submit transaction: ${result.status}`);
}

return new Commitment(requestId, transactionData, authenticator);
}
}
2 changes: 1 addition & 1 deletion src/predicate/DefaultPredicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export abstract class DefaultPredicate implements IPredicate {
CborEncoder.encodeTextString(this.type),
CborEncoder.encodeByteString(this.publicKey),
CborEncoder.encodeTextString(this.algorithm),
CborEncoder.encodeTextString(HashAlgorithm[this.hashAlgorithm]),
CborEncoder.encodeUnsignedInteger(this.hashAlgorithm),
CborEncoder.encodeByteString(this.nonce),
]);
}
Expand Down
4 changes: 2 additions & 2 deletions src/predicate/MaskedPredicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export class MaskedPredicate extends DefaultPredicate {
throw new Error(`Invalid predicate type: expected ${PredicateType.MASKED}, got ${type}`);
}

const hashAlgorithm = CborDecoder.readTextString(data[3]) as unknown as HashAlgorithm;
if (!HashAlgorithm[hashAlgorithm]) {
const hashAlgorithm = Number(CborDecoder.readUnsignedInteger(data[3]));
if (HashAlgorithm[hashAlgorithm] == null) {
throw new Error(`Invalid hash algorithm: ${hashAlgorithm}`);
}

Expand Down
4 changes: 2 additions & 2 deletions src/predicate/UnmaskedPredicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ export class UnmaskedPredicate extends DefaultPredicate {
throw new Error(`Invalid predicate type: expected ${PredicateType.UNMASKED}, got ${type}`);
}

const hashAlgorithm = CborDecoder.readTextString(data[3]) as unknown as HashAlgorithm;
if (!HashAlgorithm[hashAlgorithm]) {
const hashAlgorithm = Number(CborDecoder.readUnsignedInteger(data[3]));
if (HashAlgorithm[hashAlgorithm] == null) {
throw new Error(`Invalid hash algorithm: ${hashAlgorithm}`);
}

Expand Down
89 changes: 89 additions & 0 deletions src/serializer/cbor/token/TokenCborSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { CborDecoder } from '@unicitylabs/commons/lib/cbor/CborDecoder.js';
import { CborEncoder } from '@unicitylabs/commons/lib/cbor/CborEncoder.js';

import { TokenStateCborSerializer } from './TokenStateCborSerializer.js';
import { ISerializable } from '../../../ISerializable.js';
import { IPredicateFactory } from '../../../predicate/IPredicateFactory.js';
import { Token, TOKEN_VERSION } from '../../../token/Token.js';
import { MintTransactionData } from '../../../transaction/MintTransactionData.js';
import { Transaction } from '../../../transaction/Transaction.js';
import { TransactionData } from '../../../transaction/TransactionData.js';
import { ITokenSerializer } from '../../token/ITokenSerializer.js';
import { MintTransactionCborSerializer } from '../transaction/MintTransactionCborSerializer.js';
import { TransactionCborSerializer } from '../transaction/TransactionCborSerializer.js';

/**
* A serializer for {@link Token} objects using CBOR encoding.
* Handles serialization and deserialization of tokens, including their transactions and state.
*/
export class TokenCborSerializer implements ITokenSerializer {
private readonly mintTransactionSerializer: MintTransactionCborSerializer;
private readonly transactionSerializer: TransactionCborSerializer;
private stateSerializer: TokenStateCborSerializer;

/**
* Constructs a new `TokenCborSerializer` instance.
*
* @param {IPredicateFactory} predicateFactory - A factory for creating predicates used in token serialization.
*/
public constructor(private readonly predicateFactory: IPredicateFactory) {
this.mintTransactionSerializer = new MintTransactionCborSerializer(this);
this.transactionSerializer = new TransactionCborSerializer(predicateFactory);
this.stateSerializer = new TokenStateCborSerializer(predicateFactory);
}

/**
* Serializes a `Token` object into a CBOR-encoded byte array.
*
* @param {Token<Transaction<MintTransactionData<ISerializable | null>>>} token - The token to serialize.
* @returns {Uint8Array} The CBOR-encoded representation of the token.
*/
public static serialize(token: Token<Transaction<MintTransactionData<ISerializable | null>>>): Uint8Array {
return CborEncoder.encodeArray([
CborEncoder.encodeTextString(token.version),
MintTransactionCborSerializer.serialize(token.genesis),
CborEncoder.encodeArray(
token.transactions.map((transaction) => TransactionCborSerializer.serialize(transaction)),
),
TokenStateCborSerializer.serialize(token.state),
CborEncoder.encodeArray(token.nametagTokens.map((token) => token.toCBOR())),
]);
}

/**
* Deserializes a CBOR-encoded `Uint8Array` into a `Token` object.
*
* @param {Uint8Array} bytes - The CBOR-encoded data to deserialize.
* @returns {Promise<Token<Transaction<MintTransactionData<ISerializable | null>>>>}
* A promise that resolves to the deserialized `Token` object.
* @throws {Error} If the token version does not match the expected version.
*/
public async deserialize(bytes: Uint8Array): Promise<Token<Transaction<MintTransactionData<ISerializable | null>>>> {
const data = CborDecoder.readArray(bytes);
const tokenVersion = CborDecoder.readTextString(data[0]);
if (tokenVersion !== TOKEN_VERSION) {
throw new Error(`Cannot parse token. Version mismatch: ${tokenVersion} !== ${TOKEN_VERSION}`);
}

const mintTransaction = await this.mintTransactionSerializer.deserialize(data[1]);
const transactions: Transaction<TransactionData>[] = [];
for (const transaction of CborDecoder.readArray(data[2])) {
transactions.push(
await this.transactionSerializer.deserialize(
mintTransaction.data.tokenId,
mintTransaction.data.tokenType,
transaction,
),
);
}

// TODO: Add nametag tokens
return new Token(
await this.stateSerializer.deserialize(mintTransaction.data.tokenId, mintTransaction.data.tokenType, data[3]),
mintTransaction,
transactions,
[],
tokenVersion,
);
}
}
49 changes: 49 additions & 0 deletions src/serializer/cbor/token/TokenStateCborSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CborDecoder } from '@unicitylabs/commons/lib/cbor/CborDecoder.js';
import { CborEncoder } from '@unicitylabs/commons/lib/cbor/CborEncoder.js';

import { IPredicateFactory } from '../../../predicate/IPredicateFactory.js';
import { TokenId } from '../../../token/TokenId.js';
import { TokenState } from '../../../token/TokenState.js';
import { TokenType } from '../../../token/TokenType.js';

/**
* A serializer for {@link TokenState} objects using CBOR encoding.
* Handles serialization and deserialization of token states.
*/
export class TokenStateCborSerializer {
/**
* Constructs a new `TokenStateCborSerializer` instance.
*
* @param {IPredicateFactory} predicateFactory - A factory for creating predicates used in token state deserialization.
*/
public constructor(private readonly predicateFactory: IPredicateFactory) {}

/**
* Serializes a `TokenState` object into a CBOR-encoded byte array.
*
* @param {TokenState} state - The token state to serialize.
* @returns {Uint8Array} The CBOR-encoded representation of the token state.
*/
public static serialize(state: TokenState): Uint8Array {
return CborEncoder.encodeArray([
state.unlockPredicate.toCBOR(),
CborEncoder.encodeOptional(state.data, CborEncoder.encodeByteString),
]);
}

/**
* Deserializes a CBOR-encoded `Uint8Array` into a `TokenState` object.
*
* @param {TokenId} tokenId - The ID of the token associated with the state.
* @param {TokenType} tokenType - The type of the token associated with the state.
* @param {Uint8Array} bytes - The CBOR-encoded data to deserialize.
* @returns {Promise<TokenState>} A promise that resolves to the deserialized `TokenState` object.
*/
public async deserialize(tokenId: TokenId, tokenType: TokenType, bytes: Uint8Array): Promise<TokenState> {
const data = CborDecoder.readArray(bytes);
return TokenState.create(
await this.predicateFactory.create(tokenId, tokenType, data[0]),
CborDecoder.readOptional(data[1], CborDecoder.readByteString),
);
}
}
63 changes: 63 additions & 0 deletions src/serializer/cbor/transaction/CommitmentCborSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Authenticator } from '@unicitylabs/commons/lib/api/Authenticator.js';
import { RequestId } from '@unicitylabs/commons/lib/api/RequestId.js';
import { CborDecoder } from '@unicitylabs/commons/lib/cbor/CborDecoder.js';
import { CborEncoder } from '@unicitylabs/commons/lib/cbor/CborEncoder.js';

import { TransactionDataCborSerializer } from './TransactionDataCborSerializer.js';
import { IPredicateFactory } from '../../../predicate/IPredicateFactory.js';
import { TokenId } from '../../../token/TokenId.js';
import { TokenType } from '../../../token/TokenType.js';
import { Commitment } from '../../../transaction/Commitment.js';
import { TransactionData } from '../../../transaction/TransactionData.js';

/**
* A serializer for {@link Commitment} objects using CBOR encoding.
* Handles serialization and deserialization of commitments, including their associated transaction data.
*/
export class CommitmentCborSerializer {
private readonly transactionDataSerializer: TransactionDataCborSerializer;

/**
* Constructs a new `CommitmentCborSerializer` instance.
*
* @param {IPredicateFactory} predicateFactory - A factory for creating predicates used in transaction data deserialization.
*/
public constructor(predicateFactory: IPredicateFactory) {
this.transactionDataSerializer = new TransactionDataCborSerializer(predicateFactory);
}

/**
* Serializes a {@link Commitment} object into a CBOR-encoded byte array.
*
* @param {Commitment<TransactionData>} commitment - The commitment to serialize.
* @returns {Uint8Array} The CBOR-encoded representation of the commitment.
*/
public static serialize(commitment: Commitment<TransactionData>): Uint8Array {
return CborEncoder.encodeArray([
commitment.requestId.toCBOR(),
TransactionDataCborSerializer.serialize(commitment.transactionData),
commitment.authenticator.toCBOR(),
]);
}

/**
* Deserializes a CBOR-encoded byte array into a {@link Commitment} object.
*
* @param {TokenId} tokenId - The ID of the token associated with the commitment.
* @param {TokenType} tokenType - The type of the token associated with the commitment.
* @param {Uint8Array} bytes - The CBOR-encoded data to deserialize.
* @returns {Promise<Commitment<TransactionData>>} A promise that resolves to the deserialized `Commitment` object.
*/
public async deserialize(
tokenId: TokenId,
tokenType: TokenType,
bytes: Uint8Array,
): Promise<Commitment<TransactionData>> {
const data = CborDecoder.readArray(bytes);
return new Commitment(
RequestId.fromCBOR(data[0]),
await this.transactionDataSerializer.deserialize(tokenId, tokenType, data[1]),
Authenticator.fromCBOR(data[2]),
);
}
}
Loading