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
12 changes: 12 additions & 0 deletions contracts/nft-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,18 @@ impl ClipsNftContract {
storage::get_token(&env, token_id).map(|t| t.creator)
}

/// Return the original ClipCash backend clip ID stored at mint time (Issue #674).
///
/// Every NFT records the database Clip ID that was passed to `mint()` or
/// `batch_mint()`. This creates a verifiable on-chain link between the NFT
/// and the ClipCash database record, enabling ownership and royalty checks
/// that cross the Web2/Web3 boundary.
///
/// Returns `None` when the token does not exist.
pub fn get_clip_id(env: Env, token_id: u64) -> Option<String> {
storage::get_token(&env, token_id).map(|t| t.clip_id)
}

pub fn balance_of(env: Env, owner: Address) -> u64 {
storage::get_owner_tokens(&env, &owner).len() as u64
}
Expand Down
57 changes: 57 additions & 0 deletions src/nft/admin-contract.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,63 @@ export class AdminContractService {
};
}

/**
* Query the on-chain `get_clip_id(token_id)` view function (Issue #674).
*
* Every NFT stores the ClipCash database Clip ID passed at mint time, so
* this call lets any back-end service verify the NFT ↔ database link
* without trusting off-chain metadata.
*
* Returns `null` when the token does not exist on-chain.
*/
async getClipId(tokenId: number): Promise<{ tokenId: number; clipId: string | null }> {
const server = new StellarSdk.rpc.Server(this.stellarService.rpcUrl);
const contract = new StellarSdk.Contract(this.CONTRACT_ID);
const op = contract.call(
'get_clip_id',
StellarSdk.nativeToScVal(BigInt(tokenId), { type: 'u64' }),
);

const dummyAccount = new StellarSdk.Account(
'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN',
'0',
);

const tx = new StellarSdk.TransactionBuilder(dummyAccount, {
fee: '100',
networkPassphrase: this.stellarService.networkPassphrase,
})
.addOperation(op)
.setTimeout(StellarSdk.TimeoutInfinite)
.build();

let simulation: Awaited<ReturnType<typeof server.simulateTransaction>>;
try {
simulation = await this.circuitBreakerService.execute(
this.sorobanCircuitBreakerConfig,
async () => server.simulateTransaction(tx),
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(`Failed to query get_clip_id for token ${tokenId}: ${msg}`);
throw new InternalServerErrorException(
`Failed to query clip ID for token ${tokenId}: ${msg}`,
);
}

const results = (simulation as { results?: Array<{ xdr: string }> }).results;
if (!results?.[0]?.xdr) {
return { tokenId, clipId: null };
}

const returnValue = StellarSdk.xdr.ScVal.fromXDR(results[0].xdr, 'base64');
const native = StellarSdk.scValToNative(returnValue);
// Soroban Option<String> is either a string value or null/undefined
const clipId = native != null ? String(native) : null;

return { tokenId, clipId };
}

async getPauseStatus(): Promise<{ paused: boolean }> {
const server = new StellarSdk.rpc.Server(this.stellarService.rpcUrl);
const contract = new StellarSdk.Contract(this.CONTRACT_ID);
Expand Down
6 changes: 5 additions & 1 deletion src/nft/dto/nft-swagger.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ export class NftPrepareMintResponseDto {
xdr: string;

@ApiProperty({
description: 'Clip ID being minted',
description:
'ClipCash database Clip ID — stored on-chain inside `TokenData.clip_id` ' +
'so every NFT carries a verifiable link back to the backend record. ' +
'Query the on-chain value at any time via the `get_clip_id(token_id)` ' +
'contract view function (Issue #674).',
example: 42,
})
clipId: number;
Expand Down
30 changes: 30 additions & 0 deletions src/nft/nft.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,11 +832,11 @@
description: 'Only the current recipient can update the royalty recipient address',
})
@ApiNotFoundResponse({ description: 'NFT token not found' })
async updateRoyaltyRecipient(

Check failure on line 835 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

Declaration expected.
@Param('id', ParseIntPipe) id: number,

Check failure on line 836 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

',' expected.

Check failure on line 836 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

',' expected.

Check failure on line 836 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

Expression expected.
@Body() dto: UpdateRoyaltyRecipientDto,

Check failure on line 837 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

',' expected.

Check failure on line 837 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

',' expected.

Check failure on line 837 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

Expression expected.
): Promise<UpdateRoyaltyRecipientResponseDto> {

Check failure on line 838 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

';' expected.
const tokenIdStr = id.toString();

Check failure on line 839 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

',' expected.

Check failure on line 839 in src/nft/nft.controller.ts

View workflow job for this annotation

GitHub Actions / test

':' expected.
return this.nftService.updateRoyaltyRecipient(
tokenIdStr,
dto.newRecipient,
Expand Down Expand Up @@ -870,6 +870,36 @@
};
}

/**
* GET /nfts/tokens/:tokenId/clip-id
*
* Queries the on-chain `get_clip_id(token_id)` view function (Issue #674).
* Returns the original ClipCash database Clip ID stored inside the NFT at
* mint time, providing a verifiable on-chain ↔ database link.
*/
@Get('tokens/:tokenId/clip-id')
@ApiOperation({
summary: 'Get the original ClipCash Clip ID stored on-chain for an NFT (Issue #674)',
description:
'Calls the Soroban `get_clip_id(token_id)` view function. Every NFT stores ' +
'the ClipCash database Clip ID passed at mint time inside `TokenData.clip_id`, ' +
'so ownership and royalty checks can cross the Web2/Web3 boundary without ' +
'trusting off-chain metadata. Returns `null` when the token does not exist.',
})
@ApiParam({ name: 'tokenId', description: 'On-chain token ID (equals Clip ID)', example: 42 })
@ApiOkResponse({
description: 'Clip ID returned successfully',
schema: {
example: { tokenId: 42, clipId: '42' },
},
})
@ApiNotFoundResponse({ description: 'Token does not exist on-chain' })
async getClipId(
@Param('tokenId', ParseIntPipe) tokenId: number,
): Promise<{ tokenId: number; clipId: string | null }> {
return this.adminContractService.getClipId(tokenId);
}

@Get('gas-stats')
@ApiOperation({
summary: 'Get gas usage monitoring metrics and benchmarks (Issue #684)',
Expand Down
Loading