Skip to content

Commit 941042b

Browse files
committed
various fixes and improvements to pipe console
1 parent 7574c0e commit 941042b

File tree

2 files changed

+109
-79
lines changed

2 files changed

+109
-79
lines changed

docs/app.js

Lines changed: 108 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { connect, isConnected, request } from "https://esm.sh/@stacks/connect?bundle&target=es2020";
1+
import {
2+
connect,
3+
disconnect,
4+
isConnected,
5+
request,
6+
} from "https://esm.sh/@stacks/connect?bundle&target=es2020";
7+
import { createNetwork } from "https://esm.sh/@stacks/network@7.2.0?bundle&target=es2020";
28
import {
39
Cl,
410
Pc,
511
cvToJSON,
6-
deserializeCV,
12+
fetchCallReadOnlyFunction,
713
principalCV,
814
serializeCV,
915
} from "https://esm.sh/@stacks/transactions@7.2.0?bundle&target=es2020";
@@ -86,6 +92,7 @@ const elements = {
8692
transferValidAfter: document.getElementById("transfer-valid-after"),
8793
walletStatus: document.getElementById("wallet-status"),
8894
connectWallet: document.getElementById("connect-wallet"),
95+
disconnectWallet: document.getElementById("disconnect-wallet"),
8996
getPipe: document.getElementById("get-pipe"),
9097
openPipe: document.getElementById("open-pipe"),
9198
forceCancel: document.getElementById("force-cancel"),
@@ -108,7 +115,18 @@ function normalizedText(value) {
108115
}
109116

110117
function isStacksAddress(value) {
111-
return /^S[PMT][A-Z0-9]{38,42}$/i.test(normalizedText(value));
118+
return /^S[PMTN][A-Z0-9]{38,42}$/i.test(normalizedText(value));
119+
}
120+
121+
function isAddressOnNetwork(address, network = readNetwork()) {
122+
const text = normalizedText(address).toUpperCase();
123+
if (!text) {
124+
return false;
125+
}
126+
if (network === "mainnet") {
127+
return /^S[PM]/.test(text);
128+
}
129+
return /^S[TN]/.test(text);
112130
}
113131

114132
function isPrincipalText(value) {
@@ -219,15 +237,6 @@ function unwrapClarityJson(value) {
219237
return output;
220238
}
221239

222-
function decodeReadOnlyResult(resultHex) {
223-
const hex = normalizedText(resultHex);
224-
if (!hex) {
225-
return null;
226-
}
227-
const decoded = deserializeCV(hex);
228-
return unwrapClarityJson(cvToJSON(decoded));
229-
}
230-
231240
function parseContractId() {
232241
const raw = normalizedText(elements.contractId.value);
233242
if (!raw.includes(".")) {
@@ -431,33 +440,35 @@ function parseValidAfterCV() {
431440
return { cv: Cl.some(Cl.uint(value)), text: value.toString(10) };
432441
}
433442

434-
function extractAddress(response) {
443+
function extractAddress(response, network = readNetwork()) {
435444
const seen = new Set();
445+
const found = [];
436446
const crawl = (value) => {
437-
if (value == null) return null;
438-
if (typeof value === "string" && isStacksAddress(value)) return value;
439-
if (typeof value !== "object") return null;
440-
if (seen.has(value)) return null;
447+
if (value == null) return;
448+
if (typeof value === "string" && isStacksAddress(value)) {
449+
found.push(value);
450+
return;
451+
}
452+
if (typeof value !== "object") return;
453+
if (seen.has(value)) return;
441454
seen.add(value);
442455

443456
if (Array.isArray(value)) {
444457
for (const entry of value) {
445-
const found = crawl(entry);
446-
if (found) return found;
458+
crawl(entry);
447459
}
448-
return null;
460+
return;
449461
}
450462

451463
if (typeof value.address === "string" && isStacksAddress(value.address)) {
452-
return value.address;
464+
found.push(value.address);
453465
}
454466
for (const nested of Object.values(value)) {
455-
const found = crawl(nested);
456-
if (found) return found;
467+
crawl(nested);
457468
}
458-
return null;
459469
};
460-
return crawl(response);
470+
crawl(response);
471+
return found.find((address) => isAddressOnNetwork(address, network)) || found[0] || null;
461472
}
462473

463474
function extractSignature(response) {
@@ -495,7 +506,7 @@ async function ensureWallet({ interactive }) {
495506

496507
if (connected || interactive) {
497508
const addresses = await request("getAddresses");
498-
const address = extractAddress(addresses);
509+
const address = extractAddress(addresses, readNetwork());
499510
if (!address) {
500511
throw new Error("No Stacks address was returned by the wallet");
501512
}
@@ -581,59 +592,80 @@ async function handleConnectWallet() {
581592
}
582593
}
583594

584-
function toClarityPrincipalLiteral(principal) {
585-
return `'${normalizedText(principal)}`;
586-
}
595+
async function handleDisconnectWallet() {
596+
const previousAddress = state.connectedAddress;
597+
try {
598+
await disconnect();
599+
} catch {
600+
// Some providers may not implement explicit disconnect cleanly; still clear local state.
601+
}
587602

588-
function toOptionalPrincipalLiteral(principalOrNull) {
589-
const text = normalizedText(principalOrNull);
590-
return text ? `(some '${text})` : "none";
603+
state.connectedAddress = null;
604+
state.lastSignature = null;
605+
if (previousAddress && normalizedText(elements.forPrincipal.value) === previousAddress) {
606+
elements.forPrincipal.value = "";
607+
}
608+
if (previousAddress && normalizedText(elements.transferActor.value) === previousAddress) {
609+
elements.transferActor.value = "";
610+
}
611+
setWalletStatus("Wallet not connected.");
612+
appendLog(previousAddress ? `Wallet disconnected: ${previousAddress}` : "Wallet disconnected.");
591613
}
592614

593-
async function postReadOnlyCall(url, sender, encodedArgs) {
594-
const response = await fetch(url, {
595-
method: "POST",
596-
headers: { "content-type": "application/json" },
597-
body: JSON.stringify({
598-
sender,
599-
arguments: encodedArgs,
600-
}),
601-
});
602-
const body = await response.json().catch(() => null);
603-
return { response, body };
604-
}
615+
function getReadOnlySenderCandidates(sender, contractAddress, network = readNetwork()) {
616+
const candidates = [];
617+
const senderText = normalizedText(sender);
618+
const contractAddressText = normalizedText(contractAddress);
605619

606-
function readOnlyErrorMessage(status, body) {
607-
if (!body || typeof body !== "object") {
608-
return `Read-only call failed (${status})`;
620+
if (senderText && isAddressOnNetwork(senderText, network)) {
621+
candidates.push(senderText);
622+
}
623+
if (
624+
contractAddressText &&
625+
isAddressOnNetwork(contractAddressText, network) &&
626+
!candidates.includes(contractAddressText)
627+
) {
628+
candidates.push(contractAddressText);
629+
}
630+
if (senderText && !candidates.includes(senderText)) {
631+
candidates.push(senderText);
609632
}
610-
if (body.okay === false) {
611-
return `Read-only call returned error: ${body.cause || "unknown"}`;
633+
if (contractAddressText && !candidates.includes(contractAddressText)) {
634+
candidates.push(contractAddressText);
612635
}
613-
return `Read-only call failed (${status})`;
636+
return candidates;
614637
}
615638

616-
async function fetchReadOnly(functionName, functionArgs, sender, options = {}) {
639+
async function fetchReadOnly(functionName, functionArgs, sender) {
617640
const { contractAddress, contractName } = parseContractId();
618-
const apiBase = getStacksApiBase();
619-
const url = `${apiBase}/v2/contracts/call-read/${contractAddress}/${contractName}/${functionName}`;
620-
const hexArgs = functionArgs.map((cv) => cvHex(cv));
621-
const firstAttempt = await postReadOnlyCall(url, sender, hexArgs);
622-
if (firstAttempt.response.ok && firstAttempt.body && firstAttempt.body.okay !== false) {
623-
return firstAttempt.body.result;
624-
}
625-
626-
const clarityArgs = Array.isArray(options.clarityArgs) ? options.clarityArgs : null;
627-
if (clarityArgs && clarityArgs.length === functionArgs.length) {
628-
const secondAttempt = await postReadOnlyCall(url, sender, clarityArgs);
629-
if (secondAttempt.response.ok && secondAttempt.body && secondAttempt.body.okay !== false) {
630-
appendLog("Read-only call retried with Clarity literal arguments.");
631-
return secondAttempt.body.result;
641+
const network = createNetwork({
642+
network: readNetwork(),
643+
client: { baseUrl: getStacksApiBase() },
644+
});
645+
const senders = getReadOnlySenderCandidates(sender, contractAddress);
646+
647+
let lastError = null;
648+
for (const senderCandidate of senders) {
649+
try {
650+
const result = await fetchCallReadOnlyFunction({
651+
network,
652+
senderAddress: senderCandidate,
653+
contractAddress,
654+
contractName,
655+
functionName,
656+
functionArgs,
657+
});
658+
if (senderCandidate !== sender) {
659+
appendLog(`Read-only ${functionName} used fallback sender=${senderCandidate}.`);
660+
}
661+
return result;
662+
} catch (error) {
663+
lastError = error;
632664
}
633-
throw new Error(readOnlyErrorMessage(secondAttempt.response.status, secondAttempt.body));
634665
}
635-
636-
throw new Error(readOnlyErrorMessage(firstAttempt.response.status, firstAttempt.body));
666+
const message =
667+
lastError instanceof Error ? lastError.message : "Read-only call failed for all sender candidates";
668+
throw new Error(message);
637669
}
638670

639671
async function handleGetPipe() {
@@ -646,20 +678,15 @@ async function handleGetPipe() {
646678
"For Principal",
647679
elements.forPrincipal.value || state.connectedAddress,
648680
);
649-
const { cv: tokenCV, tokenText } = parseOptionalTokenCV();
681+
const { cv: tokenCV } = parseOptionalTokenCV();
650682

651-
const resultHex = await fetchReadOnly(
683+
const resultCv = await fetchReadOnly(
652684
"get-pipe",
653685
[tokenCV, Cl.principal(withPrincipal)],
654686
forPrincipal,
655-
{
656-
clarityArgs: [
657-
toOptionalPrincipalLiteral(tokenText),
658-
toClarityPrincipalLiteral(withPrincipal),
659-
],
660-
},
661687
);
662-
const decoded = decodeReadOnlyResult(resultHex);
688+
const resultHex = cvHex(resultCv);
689+
const decoded = unwrapClarityJson(cvToJSON(resultCv));
663690
setOutput({
664691
call: "get-pipe",
665692
forPrincipal,
@@ -836,6 +863,7 @@ async function handleSignTransfer() {
836863
await ensureWallet({ interactive: true });
837864
const context = await buildTransferContext();
838865
const response = await request("stx_signStructuredMessage", {
866+
network: readNetwork(),
839867
domain: context.domain,
840868
message: context.message,
841869
});
@@ -857,7 +885,7 @@ async function handleSignTransfer() {
857885
actor: context.actor,
858886
hashedSecret: context.hashedSecret,
859887
validAfter: context.validAfter,
860-
theirSignature: signature,
888+
signature,
861889
};
862890
state.lastPayload = payload;
863891
setOutput(payload);
@@ -884,7 +912,7 @@ async function handleBuildPayload() {
884912
actor: context.actor,
885913
hashedSecret: context.hashedSecret,
886914
validAfter: context.validAfter,
887-
theirSignature: state.lastSignature,
915+
signature: state.lastSignature,
888916
};
889917
state.lastPayload = payload;
890918
setOutput(payload);
@@ -908,6 +936,7 @@ async function handleCopyOutput() {
908936

909937
function wireEvents() {
910938
elements.connectWallet.addEventListener("click", handleConnectWallet);
939+
elements.disconnectWallet.addEventListener("click", handleDisconnectWallet);
911940
elements.getPipe.addEventListener("click", handleGetPipe);
912941
elements.openPipe.addEventListener("click", handleOpenPipe);
913942
elements.forceCancel.addEventListener("click", handleForceCancel);

docs/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ <h2>Wallet</h2>
6262
<p id="wallet-status">Wallet not connected.</p>
6363
<div class="actions">
6464
<button id="connect-wallet" type="button">Connect Wallet</button>
65+
<button id="disconnect-wallet" type="button">Disconnect Wallet</button>
6566
</div>
6667
</section>
6768

0 commit comments

Comments
 (0)