Skip to content

Commit 487059b

Browse files
committed
add support for parityUSD extension
1 parent b9b1303 commit 487059b

File tree

11 files changed

+1066
-13
lines changed

11 files changed

+1066
-13
lines changed

src/components/tokenItems/nftChild.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
const activeAction = ref<'sending' | 'minting' | 'burning' | null>(null);
4949
const parseResult = ref(undefined as ParseResult | undefined);
5050
51+
const hasParityusdExtension = computed(() => {
52+
const category = nftData.value.token?.category;
53+
return category ? !!store.bcmrRegistries?.[category]?.extensions?.parityusd : false;
54+
});
5155
const isParsable = computed(() => {
5256
const category = nftData.value.token?.category;
5357
if (!category) return false;
@@ -89,20 +93,20 @@
8993
return commitment;
9094
})
9195
92-
onMounted(() => {
96+
onMounted(async () => {
9397
const category = nftData.value.token!.category;
9498
appendBlockieIcon(category, `#${id.value}`);
9599
// Parse NFT commitment if this is a parsable NFT
96100
if (isParsable.value) {
97-
parseResult.value = store.parseNftCommitment(category, nftData.value);
101+
parseResult.value = await store.parseNftCommitment(category, nftData.value);
98102
}
99103
})
100104
101105
// Watch for isParsable becoming true after mount (e.g. bcmrRegistries loads async)
102-
watch(isParsable, (nowParsable) => {
106+
watch(isParsable, async (nowParsable) => {
103107
if (nowParsable && !parseResult.value) {
104108
const category = nftData.value.token!.category;
105-
parseResult.value = store.parseNftCommitment(category, nftData.value);
109+
parseResult.value = await store.parseNftCommitment(category, nftData.value);
106110
}
107111
})
108112
@@ -464,7 +468,7 @@
464468
<div v-if="displayNftInfo" class="tokenAction">
465469
<div v-if="nftDescription" class="indentText"> {{ t('tokenItem.info.nftDescription') }} {{ nftDescription }} </div>
466470
<div v-if="parseResult?.success && parseResult.namedFields?.length && parseResult.namedFields.length > 3">
467-
<div>{{ t('tokenItem.info.parsedFields') }}</div>
471+
<div>{{ hasParityusdExtension ? t('tokenItem.info.extensionNote') : t('tokenItem.info.parsedFields') }}</div>
468472
<div v-for="(field, index) in parseResult.namedFields" :key="'parsed-field-' + index" style="white-space: pre-wrap; margin-left:15px">
469473
{{ field.name ?? field.fieldId ?? `Field ${index}` }}: {{ field.parsedValue?.formatted ?? field.value }}
470474
</div>

src/components/tokenItems/tokenItemNFT.vue

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
const imageLoadFailed = ref(false);
5656
const parseResult = ref(undefined as ParseResult | undefined);
5757
58+
const hasParityusdExtension = computed(() => {
59+
return !!store.bcmrRegistries?.[tokenData.value.category]?.extensions?.parityusd;
60+
});
5861
const isParsable = computed(() =>
5962
store.bcmrRegistries?.[tokenData.value.category]?.nft_type === 'parsable'
6063
);
@@ -130,23 +133,23 @@
130133
selectedNfts.value = new Set(allKeys);
131134
}
132135
133-
onMounted(() => {
136+
onMounted(async () => {
134137
appendBlockieIcon(tokenData.value.category, `#id${tokenData.value.category.slice(0, 10)}nft`);
135138
// Parse NFT commitment if this is a parsable single NFT
136139
if (isSingleNft.value && isParsable.value) {
137140
const nftUtxo = tokenData.value.nfts?.[0];
138141
if (nftUtxo) {
139-
parseResult.value = store.parseNftCommitment(tokenData.value.category, nftUtxo);
142+
parseResult.value = await store.parseNftCommitment(tokenData.value.category, nftUtxo);
140143
}
141144
}
142145
})
143146
144147
// Watch for isParsable becoming true after mount (e.g. bcmrRegistries loads async)
145-
watch(isParsable, (nowParsable) => {
148+
watch(isParsable, async (nowParsable) => {
146149
if (nowParsable && isSingleNft.value && !parseResult.value) {
147150
const nftUtxo = tokenData.value.nfts?.[0];
148151
if (nftUtxo) {
149-
parseResult.value = store.parseNftCommitment(tokenData.value.category, nftUtxo);
152+
parseResult.value = await store.parseNftCommitment(tokenData.value.category, nftUtxo);
150153
}
151154
}
152155
})
@@ -726,7 +729,7 @@
726729
<div></div>
727730
<div v-if="tokenDescription" class="indentText">{{ t('tokenItem.info.tokenDescription') }} {{ tokenDescription }} </div>
728731
<div v-if="parseResult?.success && parseResult.namedFields?.length && parseResult.namedFields.length > 2">
729-
<div>{{ t('tokenItem.info.parsedFields') }}</div>
732+
<div>{{ hasParityusdExtension ? t('tokenItem.info.extensionNote') : t('tokenItem.info.parsedFields') }}</div>
730733
<div v-for="(field, index) in parseResult.namedFields" :key="'parsed-field-' + index" style="white-space: pre-wrap; margin-left:15px">
731734
{{ field.name ?? field.fieldId ?? `Field ${index}` }}: {{ field.parsedValue?.formatted ?? field.value }}
732735
</div>

src/i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
"yes": "yes",
214214
"no": "no",
215215
"nftAttributes": "NFT attributes",
216+
"extensionNote": "Loan data fetched from blockchain:",
216217
"parsedFields": "Parsed NFT data:",
217218
"numberNfts": "Number NFTs:"
218219
},

src/i18n/locales/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
"yes": "",
214214
"no": "no",
215215
"nftAttributes": "Atributos del NFT",
216+
"extensionNote": "Datos del préstamo obtenidos de la blockchain:",
216217
"parsedFields": "Datos NFT analizados:",
217218
"numberNfts": "Número de NFTs:"
218219
},

src/i18n/locales/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
"yes": "oui",
214214
"no": "non",
215215
"nftAttributes": "Attributs du NFT",
216+
"extensionNote": "Données du prêt récupérées de la blockchain :",
216217
"parsedFields": "Données NFT analysées :",
217218
"numberNfts": "Nombre de NFTs :"
218219
},

src/parsing/electrumAdapter.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { ElectrumClient, ElectrumUtxo } from "./extensions/types";
2+
3+
/**
4+
* Minimal provider interface — duck-typed to avoid deep imports from mainnet-js.
5+
*/
6+
interface ElectrumProvider {
7+
getUtxos(cashaddr: string): Promise<{
8+
txid: string;
9+
vout: number;
10+
satoshis: bigint;
11+
height?: number;
12+
token?: {
13+
category: string;
14+
amount: bigint;
15+
nft?: {
16+
capability: "none" | "mutable" | "minting";
17+
commitment: string;
18+
};
19+
};
20+
}[]>;
21+
getRawTransaction(txHash: string): Promise<string>;
22+
}
23+
24+
/**
25+
* Create an ElectrumClient adapter from a mainnet-js ElectrumNetworkProvider.
26+
*
27+
* Bridges the mainnet-js Utxo format to the ElectrumUtxo format expected
28+
* by the extension system.
29+
*/
30+
export function createElectrumAdapter(provider: ElectrumProvider): ElectrumClient {
31+
return {
32+
async getUTXOs(address: string): Promise<ElectrumUtxo[]> {
33+
const utxos = await provider.getUtxos(address);
34+
return utxos.map((utxo) => {
35+
const result: ElectrumUtxo = {
36+
tx_hash: utxo.txid,
37+
tx_pos: utxo.vout,
38+
value: utxo.satoshis,
39+
script: "",
40+
...(utxo.height !== undefined && { height: utxo.height }),
41+
};
42+
if (utxo.token) {
43+
result.token_data = {
44+
category: utxo.token.category,
45+
amount: utxo.token.amount?.toString(),
46+
};
47+
if (utxo.token.nft?.capability !== undefined || utxo.token.nft?.commitment !== undefined) {
48+
result.token_data.nft = {
49+
capability: utxo.token.nft?.capability ?? "none",
50+
commitment: utxo.token.nft?.commitment ?? "",
51+
};
52+
}
53+
}
54+
return result;
55+
});
56+
},
57+
58+
async getRawTransaction(txid: string): Promise<string> {
59+
return provider.getRawTransaction(txid);
60+
},
61+
};
62+
}

src/parsing/extensions/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { Output } from "@bitauth/libauth";
2+
import type { ExtensionRegistry, ElectrumClient } from "./types";
3+
import type { IdentitySnapshot } from "../bcmr-v2.schema";
4+
5+
// Import extension handlers
6+
import { fetchLoanState } from "./parityusd";
7+
8+
/**
9+
* Registry of all available extensions
10+
*/
11+
export const extensions: ExtensionRegistry = {
12+
parityusd: {
13+
fetchLoanState,
14+
},
15+
};
16+
17+
/**
18+
* Invoke extensions declared in a BCMR identity
19+
*
20+
* Extensions are called in order and can modify the UTXO before NFT parsing.
21+
* Common use case: Fetch on-chain data and transplant into UTXO commitment.
22+
*
23+
* @param utxo - The UTXO to process
24+
* @param identitySnapshot - BCMR identity snapshot containing extensions config
25+
* @param electrumClient - Electrum client for blockchain data fetching
26+
* @param networkPrefix - Network prefix ("bitcoincash" or "bchtest")
27+
* @param extensionsEnabled - Optional map of extension names to enabled status
28+
* @returns Modified UTXO with extension processing applied
29+
*/
30+
export async function invokeExtensions(
31+
utxo: Output,
32+
identitySnapshot: IdentitySnapshot,
33+
electrumClient: ElectrumClient,
34+
networkPrefix: string,
35+
extensionsEnabled?: Record<string, boolean>,
36+
): Promise<Output> {
37+
if (!identitySnapshot.extensions) {
38+
return utxo;
39+
}
40+
41+
let modifiedUtxo = utxo;
42+
43+
// Iterate through all extensions in the identity
44+
for (const [extensionName, extensionConfig] of Object.entries(
45+
identitySnapshot.extensions,
46+
)) {
47+
// Check if this extension is enabled (default to true if not specified)
48+
const isEnabled = extensionsEnabled?.[extensionName] ?? true;
49+
if (!isEnabled) {
50+
console.log(`Extension ${extensionName} is disabled, skipping`);
51+
continue;
52+
}
53+
54+
const extensionHandlers = extensions[extensionName];
55+
if (!extensionHandlers) {
56+
console.warn(`Unknown extension: ${extensionName}`);
57+
continue;
58+
}
59+
60+
// Iterate through all methods in this extension
61+
for (const methodName of Object.keys(extensionConfig as object)) {
62+
const handler = extensionHandlers[methodName];
63+
if (!handler) {
64+
console.warn(
65+
`Unknown method ${methodName} in extension ${extensionName}`,
66+
);
67+
continue;
68+
}
69+
70+
console.log(
71+
`Invoking extension: ${extensionName}.${methodName}`,
72+
);
73+
74+
try {
75+
modifiedUtxo = await handler(
76+
modifiedUtxo,
77+
identitySnapshot,
78+
electrumClient,
79+
networkPrefix,
80+
);
81+
} catch (error) {
82+
console.error(
83+
`Error invoking extension ${extensionName}.${methodName}:`,
84+
error,
85+
);
86+
// Continue with unmodified UTXO on error
87+
}
88+
}
89+
}
90+
91+
return modifiedUtxo;
92+
}
93+
94+
// Re-export types for convenience
95+
export type {
96+
ElectrumClient,
97+
ElectrumUtxo,
98+
ExtensionHandler,
99+
} from "./types";

0 commit comments

Comments
 (0)