@@ -474,12 +523,20 @@ if (window.Alpine === undefined) {
const tselElem = new TomSelect(elem, {
allowEmptyOption: empty,
render: {
- option(data, escape) { return renderSelector(data, escape); },
- item(data, escape) { return renderSelector(data, escape); },
+ loading: (data, escape) => "
⏳
",
+ option: (data, escape) => renderSelector(data, escape),
+ item: (data, escape) => renderSelector(data, escape),
+ ...(!create ? {} : {
+ no_results: (data, escape) => null,
+ option_create: (data, escape) => `
+
+ Use custom option ${escape(data.input)}
+
`,
+ }),
},
onDropdownOpen: (dropdown) => {
if (
- upOnly ||
+ forceUp ||
(dropdown.getBoundingClientRect().bottom >
(window.innerHeight || document.documentElement.clientHeight))
) {
@@ -490,8 +547,14 @@ if (window.Alpine === undefined) {
dropdown.classList.remove('dropup');
},
...(empty ? {} : {controlInput: null}),
- ...((maxItems === undefined || maxItems === 0) ? {} : {maxItems})
+ ...((maxItems === undefined || maxItems === 0) ? {} : {maxItems}),
+ ...(!load ? {} : {load}),
+ ...(!create ? {} : {
+ create: !!create,
+ onOptionAdd: create,
+ }),
});
+ tselElem?.load && tselElem.load();
elem.matches(":disabled") && tselElem.disable();
}
}
@@ -505,6 +568,7 @@ if (window.Alpine === undefined) {
text: elem.innerText,
image: elem.dataset.image,
chain: elem.dataset.chain,
+ href: elem.dataset.href,
})).filter(({value, chain}) => (
chain === tokenChain || value === ""
));
@@ -517,26 +581,57 @@ if (window.Alpine === undefined) {
);
}
- function updateNftsSelect(elem) {
- const walletNfts = document.querySelector("#fund-nfts-wallet");
- const tokenSelect = document.querySelector('#proj-token').tomselect;
- const tokenNftOpts = Array.from(walletNfts.children).map(elem => ({
- value: elem.value,
- text: elem.innerText,
- image: elem.dataset.image,
- }));
- if (tokenNftOpts.length === 0) {
- tokenNftOpts.push({
- value: "-1",
- text: "(no nfts in wallet)",
- image: "https://placehold.co/24x24/black/black?text=\\n",
- });
- }
+ function tsCreateOracle(elem) {
+ return (value, data) => {
+ const okClans = new Set(["galaxy", "star"]);
+ if (!UrbitOb.isValidPatp(value) || !okClans.has(UrbitOb.clan(value))) {
+ elem.tomselect.removeOption(value);
+ } else if (data?.image === undefined) {
+ elem.tomselect.updateOption(value, {
+ value: data.value,
+ text: data.text,
+ image: `https://azimuth.network/erc721/${UrbitOb.patp2dec(value)}.svg`,
+ });
+ }
+ };
+ }
- tokenSelect.clear(true);
- tokenSelect.clearOptions();
- tokenSelect.addOptions(tokenNftOpts);
- tokenSelect.addItem(tokenNftOpts[0].value);
+ function tsLoadNFTs(elem) {
+ return (query, callback) => {
+ const self = elem.tomselect;
+ if (self.loading > 1) return callback();
+
+ const address = Alpine.store("wallet").address;
+ const chain = Alpine.store("wallet").chain;
+ const loadedNFTs = Alpine.store("project").nfts?.[address];
+ const loadNFTOptions = loadedNFTs
+ ? Promise.resolve(loadedNFTs)
+ : SAFE.nftsGetAll(address, chain, Alpine.store("project").symbol).then(nfts => (
+ // TODO: Generalize this logic by querying metadata filters from the BE
+ nfts.filter(nft => ((nft?.raw?.metadata?.attributes ?? []).some(attr => (
+ (attr?.trait_type === "size" && attr?.value === "star")
+ )))).map(({name, image, tokenId}) => ({
+ value: tokenId,
+ text: name,
+ image: image.cachedUrl,
+ }))
+ )).catch(() => []);
+
+ self.clear(true);
+ self.clearOptions();
+ loadNFTOptions.then(options => {
+ const nftOptions = (options.length > 0) ? options : [{
+ value: "-1",
+ text: "(no nfts in wallet)",
+ image: "https://placehold.co/24x24/black/black?text=\\n",
+ }];
+ Alpine.store("project").loadNFTs(address, nftOptions);
+ callback(nftOptions);
+ // NOTE: Auto-select if only one available; iffy on the ui/ux
+ // if (nftOptions.length === 0) { self.addItem(-1); }
+ delete self.loadedSearches[query];
+ }).catch(() => callback());
+ };
}
window.Wagmi = createConfig({
@@ -614,48 +709,18 @@ if (window.Alpine === undefined) {
});
const setPageWallet = ({connections, current, status}) => {
- const walletButton = document.querySelector("#fund-butn-wallet");
- const walletNfts = document.querySelector("#fund-nfts-wallet");
-
if (status === "disconnected") {
const connection = connections.get(current);
if (!connection) {
- walletButton.innerHTML = "connect 💰";
+ Alpine.store("wallet").update(undefined, undefined);
} else {
reconnect(window.Wagmi, {connector: connection.connector});
}
} else if (status === "reconnecting") {
- walletButton.innerHTML = "…loading…";
+ Alpine.store("wallet").update(null, null);
} else if (status === "connected") {
const { address, chainId } = getAccount(window.Wagmi);
-
- walletButton.innerHTML = `${address.slice(0, 5)}…${address.slice(-4)}`;
-
- const walletSwapType = document.querySelector("#fund-swap-type");
- if (walletSwapType && walletSwapType.value === "enft") {
- const walletSwapSymbol = document.querySelector("#fund-swap-symb");
- SAFE.nftsGetAll(address, chainId, walletSwapSymbol.value).then(nfts => {
- const options = nfts
- .filter(nft => ((nft?.raw?.metadata?.attributes ?? []).some(attr => (
- (attr?.trait_type === "size" && attr?.value === "star")
- )))).map(({name, image, tokenId}) => ({
- value: tokenId,
- text: name,
- image: image.cachedUrl,
- }));
- console.log(`loading ${options.length} nfts`);
- while (walletNfts.firstChild) {
- walletNfts.removeChild(walletNfts.lastChild);
- }
- options.forEach(({value, text, image}) => {
- const elem = document.createElement("option");
- elem.setAttribute("value", value);
- elem.setAttribute("data-image", image);
- elem.innerText = text;
- walletNfts.appendChild(elem);
- });
- });
- }
+ Alpine.store("wallet").update(address, chainId);
}
};
diff --git a/desk/bare/web/fund/script/const.js b/desk/bare/web/fund/script/const.js
index dfef294..57f22d9 100644
--- a/desk/bare/web/fund/script/const.js
+++ b/desk/bare/web/fund/script/const.js
@@ -1,3 +1,5 @@
+import { FUND_ALCH_AKEY, FUND_RPCE_ETHE, FUND_RPCE_SEPO } from './config.js';
+
export const FUND_CUT = 0.01;
export const ADDRESS = Object.freeze({
@@ -16,33 +18,12 @@ export const NETWORK = Object.freeze({
11155111: "SEPOLIA",
}),
APIKEY: Object.freeze({
- ETHEREUM:
- ["3", "E", "1", "u", "G", "l", "C", "j", "i", "F", "W", "V", "n",
- "L", "r", "1", "3", "b", "b", "n", "J", "B", "F", "F", "W", "c",
- "-", "4", "W", "B", "2", "2"].join(""),
- SEPOLIA:
- ["2", "R", "E", "E", "E", "A", "S", "1", "y", "b", "f", "y", "f",
- "H", "R", "3", "4", "w", "B", "D", "F", "C", "I", "H", "C", "S",
- "X", "J", "t", "d", "f", "E"].join(""),
+ ETHEREUM: FUND_ALCH_AKEY,
+ SEPOLIA: FUND_ALCH_AKEY,
}),
RPC: Object.freeze({
- // NOTE: Make it at least nontrivial to sniff out and steal these API keys
- ETHEREUM:
- ["h", "t", "t", "p", "s", ":", "/", "/", "e", "t", "h",
- "-", "m", "a", "i", "n", "n", "e", "t", ".", "g", ".",
- "a", "l", "c", "h", "e", "m", "y", ".", "c", "o", "m",
- "/", "v", "2", "/", "3", "E", "1", "u", "G", "l", "C",
- "j", "i", "F", "W", "V", "n", "L", "r", "1", "3", "b",
- "b", "n", "J", "B", "F", "F", "W", "c", "-", "4", "W",
- "B", "2", "2"].join(""),
- SEPOLIA:
- ["h", "t", "t", "p", "s", ":", "/", "/", "e", "t", "h",
- "-", "s", "e", "p", "o", "l", "i", "a", ".", "g", ".",
- "a", "l", "c", "h", "e", "m", "y", ".", "c", "o", "m",
- "/", "v", "2", "/", "2", "R", "E", "E", "E", "A", "S",
- "1", "y", "b", "f", "y", "f", "H", "R", "3", "4", "w",
- "B", "D", "F", "C", "I", "H", "C", "S", "X", "J", "t",
- "d", "f", "E"].join(""),
+ ETHEREUM: FUND_RPCE_ETHE,
+ SEPOLIA: FUND_RPCE_SEPO,
}),
});
diff --git a/desk/bare/web/fund/script/safe.js b/desk/bare/web/fund/script/safe.js
index 0aa7725..2516702 100644
--- a/desk/bare/web/fund/script/safe.js
+++ b/desk/bare/web/fund/script/safe.js
@@ -73,7 +73,7 @@ export const nftsGetAll = async (wallet, chainId, token) => {
queryUrl.searchParams.append("pageKey", pageKey);
}
return isLastCall
- ? new Promise(resolve => resolve(results))
+ ? Promise.resolve(results)
: fetch(queryUrl)
.then(response => response.json())
.then(json => getNFTs(
@@ -103,7 +103,7 @@ export const safeGetBalance = async ({fundToken, safeAddress}) => {
}
};
-export const safeGetTransfers = async ({fundToken, safeAddress, safeInitBlock}) => {
+export const safeGetTransfers = async ({fundToken, safeAddress, safeInitBlock, dirFilter}) => {
const TOKEN = safeTransactionToken({tok: fundToken});
// FIXME: If there are ever projects that exceed ~2k contributions, this will
// need to be changed to paginate RPC queries.
@@ -115,7 +115,12 @@ export const safeGetTransfers = async ({fundToken, safeAddress, safeInitBlock})
fromBlock: BigInt(safeInitBlock),
// toBlock: "safe",
});
- return transferLogs;
+ const filteredTransferLogs = transferLogs.filter(({args: {from, to, tokenId}}) => (
+ (dirFilter === "with") ? (from === safeAddress)
+ : (dirFilter === "depo") ? (to === safeAddress)
+ : true
+ ));
+ return filteredTransferLogs;
};
export const safeSignDeploy = async ({projectChain, projectContent}) => {
@@ -152,9 +157,7 @@ export const safeExecDeploy = async ({projectChain, oracleAddress}) => {
safeNextSaltNonce(),
],
});
- const deployReceipt = await waitForTransactionReceipt(window.Wagmi, {
- hash: deployTransaction,
- });
+ const deployReceipt = await safeAwaitTransaction({hash: deployTransaction});
const safeAddress = deployReceipt.logs.find(
({topics}) => topics.length > 1
).address;
@@ -183,9 +186,7 @@ export const safeExecDeposit = async ({projectChain, fundAmount, fundToken, fund
);
const sendTransaction = await writeContract(window.Wagmi, wrappedTransaction);
- const sendReceipt = await waitForTransactionReceipt(window.Wagmi, {
- hash: sendTransaction,
- });
+ const sendReceipt = await safeAwaitTransaction({hash: sendTransaction});
return [
funderAddress,
sendReceipt.blockNumber.toString(),
@@ -259,6 +260,27 @@ const safeAddressSort = (getAddress = (v) => v) => (a, b) => {
[a, b].map(v => fromHex(getAddress(v), "bigint")));
};
+// NOTE: Needed due to aggressive load balancing on RPC endpoints; see here:
+// https://github.com/wevm/wagmi/issues/3152
+const safeAwaitTransaction = async ({hash}) => {
+ let attempts = 0;
+ const maxAttempts = 3;
+
+ let receipt = undefined;
+ while (receipt === undefined && attempts++ < maxAttempts) {
+ try {
+ receipt = await waitForTransactionReceipt(window.Wagmi, {hash, confirmations: 3});
+ } catch (error) {
+ if (!(error instanceof TransactionNotFoundError)) throw error;
+ }
+ }
+ if (receipt === undefined) {
+ throw new SafeError(`unable to detect confirmation for transaction '${hash}'`);
+ }
+
+ return receipt;
+};
+
const safeWrapTransaction = ({tok, val, from, to}) => {
const TOKEN = safeTransactionToken({tok, val, from, to});
return {
@@ -292,6 +314,7 @@ const safeGetClaimTransactions = async ({projectChain, fundAmount, fundToken, sa
} else if (TOKEN.ABI === ABI.ERC721) {
const transfers = await safeGetTransfers({fundToken, safeAddress, safeInitBlock});
const remainingTokens = await nftsGetAll(safeAddress, projectChain, fundToken);
+ // TODO: Needs to be generalized for any NFT
const validTokenSet = new Set(remainingTokens.filter(nft => (
(nft?.raw?.metadata?.attributes ?? []).some(attr => (
(attr?.trait_type === "size" && attr?.value === "star")
@@ -338,6 +361,7 @@ const safeGetRefundTransactions = async ({projectChain, fundToken, safeAddress,
});
} else if (TOKEN.ABI === ABI.ERC721) {
const remainingTokens = await nftsGetAll(safeAddress, projectChain, fundToken);
+ // TODO: Needs to be generalized for any NFT
const validTokenSet = new Set(remainingTokens.filter(nft => (
(nft?.raw?.metadata?.attributes ?? []).some(attr => (
(attr?.trait_type === "size" && attr?.value === "star")
@@ -483,8 +507,6 @@ const safeExecWithdrawal = async ({transactions, oracleSignature, oracleAddress,
).reduce((a, n) => concat([a, n]), "")
]),
});
- const withdrawReceipt = await waitForTransactionReceipt(window.Wagmi, {
- hash: withdrawTransaction,
- });
+ const withdrawReceipt = await safeAwaitTransaction({hash: withdrawTransaction});
return [withdrawReceipt.blockNumber.toString(), withdrawReceipt.transactionHash];
};
diff --git a/desk/bare/web/fund/style/fund.css b/desk/bare/web/fund/style/fund.css
index 188e71c..003c833 100644
--- a/desk/bare/web/fund/style/fund.css
+++ b/desk/bare/web/fund/style/fund.css
@@ -75,7 +75,7 @@ input[required] + label:after,
select[required] + label:after,
div.ts-wrapper + label:after {
content: '*';
- color: #b80c09; /* text-highlight1-500 */
+ color: #ff0033; /* text-highlight1-500 */
}
/* Drop-up for selectors near the bottom of the page (https://stackoverflow.com/a/78298291) */
diff --git a/desk/full/lib/fund/alien.hoon b/desk/full/lib/fund/alien.hoon
new file mode 120000
index 0000000..eefdb68
--- /dev/null
+++ b/desk/full/lib/fund/alien.hoon
@@ -0,0 +1 @@
+../../../bare/lib/fund/alien.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/core-0.hoon b/desk/full/lib/fund/core-0.hoon
deleted file mode 120000
index c55fe10..0000000
--- a/desk/full/lib/fund/core-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/lib/fund/core-0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/core-1.hoon b/desk/full/lib/fund/core-1.hoon
deleted file mode 120000
index 15b24f3..0000000
--- a/desk/full/lib/fund/core-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/lib/fund/core-1.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/core/0.hoon b/desk/full/lib/fund/core/0.hoon
new file mode 120000
index 0000000..3d82587
--- /dev/null
+++ b/desk/full/lib/fund/core/0.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/core/0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/core/1.hoon b/desk/full/lib/fund/core/1.hoon
new file mode 120000
index 0000000..36805a7
--- /dev/null
+++ b/desk/full/lib/fund/core/1.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/core/1.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/meta-0.hoon b/desk/full/lib/fund/meta-0.hoon
deleted file mode 120000
index 6306633..0000000
--- a/desk/full/lib/fund/meta-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/lib/fund/meta-0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/meta/0.hoon b/desk/full/lib/fund/meta/0.hoon
new file mode 120000
index 0000000..ce06fff
--- /dev/null
+++ b/desk/full/lib/fund/meta/0.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/meta/0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/proj-0.hoon b/desk/full/lib/fund/proj-0.hoon
deleted file mode 120000
index a3fda56..0000000
--- a/desk/full/lib/fund/proj-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/lib/fund/proj-0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/proj-1.hoon b/desk/full/lib/fund/proj-1.hoon
deleted file mode 120000
index 04b6011..0000000
--- a/desk/full/lib/fund/proj-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/lib/fund/proj-1.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/proj/0.hoon b/desk/full/lib/fund/proj/0.hoon
new file mode 120000
index 0000000..c885219
--- /dev/null
+++ b/desk/full/lib/fund/proj/0.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/proj/0.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/proj/1.hoon b/desk/full/lib/fund/proj/1.hoon
new file mode 120000
index 0000000..2e5eaef
--- /dev/null
+++ b/desk/full/lib/fund/proj/1.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/proj/1.hoon
\ No newline at end of file
diff --git a/desk/full/lib/fund/proj/2.hoon b/desk/full/lib/fund/proj/2.hoon
new file mode 120000
index 0000000..c396128
--- /dev/null
+++ b/desk/full/lib/fund/proj/2.hoon
@@ -0,0 +1 @@
+../../../../bare/lib/fund/proj/2.hoon
\ No newline at end of file
diff --git a/desk/full/sur/contacts.hoon b/desk/full/sur/contacts.hoon
new file mode 120000
index 0000000..a8fde11
--- /dev/null
+++ b/desk/full/sur/contacts.hoon
@@ -0,0 +1 @@
+../../bare/sur/contacts.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/core-0.hoon b/desk/full/sur/fund/core-0.hoon
deleted file mode 120000
index 84286ff..0000000
--- a/desk/full/sur/fund/core-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/core-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/core-1.hoon b/desk/full/sur/fund/core-1.hoon
deleted file mode 120000
index b625e12..0000000
--- a/desk/full/sur/fund/core-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/core-1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/core/0.hoon b/desk/full/sur/fund/core/0.hoon
new file mode 120000
index 0000000..a599379
--- /dev/null
+++ b/desk/full/sur/fund/core/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/core/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/core/1.hoon b/desk/full/sur/fund/core/1.hoon
new file mode 120000
index 0000000..a1fc073
--- /dev/null
+++ b/desk/full/sur/fund/core/1.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/core/1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/data-0.hoon b/desk/full/sur/fund/data-0.hoon
deleted file mode 120000
index a49a670..0000000
--- a/desk/full/sur/fund/data-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/data-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/data-1.hoon b/desk/full/sur/fund/data-1.hoon
deleted file mode 120000
index bbec6da..0000000
--- a/desk/full/sur/fund/data-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/data-1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/data/0.hoon b/desk/full/sur/fund/data/0.hoon
new file mode 120000
index 0000000..215e745
--- /dev/null
+++ b/desk/full/sur/fund/data/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/data/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/data/1.hoon b/desk/full/sur/fund/data/1.hoon
new file mode 120000
index 0000000..a6032ef
--- /dev/null
+++ b/desk/full/sur/fund/data/1.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/data/1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/meta-0.hoon b/desk/full/sur/fund/meta-0.hoon
deleted file mode 120000
index df7ffdf..0000000
--- a/desk/full/sur/fund/meta-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/meta-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/meta/0.hoon b/desk/full/sur/fund/meta/0.hoon
new file mode 120000
index 0000000..a3eb45a
--- /dev/null
+++ b/desk/full/sur/fund/meta/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/meta/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/proj-0.hoon b/desk/full/sur/fund/proj-0.hoon
deleted file mode 120000
index 1d202c4..0000000
--- a/desk/full/sur/fund/proj-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/proj-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/proj-1.hoon b/desk/full/sur/fund/proj-1.hoon
deleted file mode 120000
index b3f7b85..0000000
--- a/desk/full/sur/fund/proj-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/fund/proj-1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/proj/0.hoon b/desk/full/sur/fund/proj/0.hoon
new file mode 120000
index 0000000..cb12063
--- /dev/null
+++ b/desk/full/sur/fund/proj/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/proj/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/proj/1.hoon b/desk/full/sur/fund/proj/1.hoon
new file mode 120000
index 0000000..32509e1
--- /dev/null
+++ b/desk/full/sur/fund/proj/1.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/proj/1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/fund/proj/2.hoon b/desk/full/sur/fund/proj/2.hoon
new file mode 120000
index 0000000..cc9f022
--- /dev/null
+++ b/desk/full/sur/fund/proj/2.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/fund/proj/2.hoon
\ No newline at end of file
diff --git a/desk/full/sur/settings.hoon b/desk/full/sur/settings.hoon
new file mode 100644
index 0000000..9ba9683
--- /dev/null
+++ b/desk/full/sur/settings.hoon
@@ -0,0 +1,44 @@
+/+ *mip
+|%
+::
+++ settings-0
+ =< settings
+ |%
+ +$ settings (map key bucket)
+ +$ bucket (map key val)
+ +$ val
+ $% [%s p=@t]
+ [%b p=?]
+ [%n p=@]
+ ==
+ --
+::
+++ settings-1
+ =< settings
+ |%
+ +$ settings (map key bucket)
+ --
++$ bucket (map key val)
++$ key term
++$ val
+ $~ [%n 0]
+ $% [%s p=@t]
+ [%b p=?]
+ [%n p=@]
+ [%a p=(list val)]
+ ==
+::
++$ settings (mip desk key bucket)
++$ event
+ $% [%put-bucket =desk =key =bucket]
+ [%del-bucket =desk =key]
+ [%put-entry =desk buc=key =key =val]
+ [%del-entry =desk buc=key =key]
+ ==
++$ data
+ $% [%all =settings]
+ [%bucket =bucket]
+ [%desk desk=(map key bucket)]
+ [%entry =val]
+ ==
+--
diff --git a/desk/full/sur/sss/meta-0.hoon b/desk/full/sur/sss/meta-0.hoon
deleted file mode 120000
index 3afdd2e..0000000
--- a/desk/full/sur/sss/meta-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/meta-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/meta/0.hoon b/desk/full/sur/sss/meta/0.hoon
new file mode 120000
index 0000000..6f76a05
--- /dev/null
+++ b/desk/full/sur/sss/meta/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/meta/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj-0.hoon b/desk/full/sur/sss/proj-0.hoon
deleted file mode 120000
index a1dafcc..0000000
--- a/desk/full/sur/sss/proj-0.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/proj-0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj-1.hoon b/desk/full/sur/sss/proj-1.hoon
deleted file mode 120000
index e0dc650..0000000
--- a/desk/full/sur/sss/proj-1.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/proj-1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj-2.hoon b/desk/full/sur/sss/proj-2.hoon
deleted file mode 120000
index 735f89c..0000000
--- a/desk/full/sur/sss/proj-2.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/proj-2.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj-3.hoon b/desk/full/sur/sss/proj-3.hoon
deleted file mode 120000
index 9569fe3..0000000
--- a/desk/full/sur/sss/proj-3.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/proj-3.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj-4.hoon b/desk/full/sur/sss/proj-4.hoon
deleted file mode 120000
index 437be8d..0000000
--- a/desk/full/sur/sss/proj-4.hoon
+++ /dev/null
@@ -1 +0,0 @@
-../../../bare/sur/sss/proj-4.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/0.hoon b/desk/full/sur/sss/proj/0.hoon
new file mode 120000
index 0000000..76d225a
--- /dev/null
+++ b/desk/full/sur/sss/proj/0.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/0.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/1.hoon b/desk/full/sur/sss/proj/1.hoon
new file mode 120000
index 0000000..53e11b7
--- /dev/null
+++ b/desk/full/sur/sss/proj/1.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/1.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/2.hoon b/desk/full/sur/sss/proj/2.hoon
new file mode 120000
index 0000000..067118c
--- /dev/null
+++ b/desk/full/sur/sss/proj/2.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/2.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/3.hoon b/desk/full/sur/sss/proj/3.hoon
new file mode 120000
index 0000000..37bd35b
--- /dev/null
+++ b/desk/full/sur/sss/proj/3.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/3.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/4.hoon b/desk/full/sur/sss/proj/4.hoon
new file mode 120000
index 0000000..0c8ff6a
--- /dev/null
+++ b/desk/full/sur/sss/proj/4.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/4.hoon
\ No newline at end of file
diff --git a/desk/full/sur/sss/proj/5.hoon b/desk/full/sur/sss/proj/5.hoon
new file mode 120000
index 0000000..baa8e11
--- /dev/null
+++ b/desk/full/sur/sss/proj/5.hoon
@@ -0,0 +1 @@
+../../../../bare/sur/sss/proj/5.hoon
\ No newline at end of file
diff --git a/meta/docs/test.md b/meta/docs/test.md
index d6444f9..64bd717 100644
--- a/meta/docs/test.md
+++ b/meta/docs/test.md
@@ -37,6 +37,7 @@ These tests must be run on `~zod` in order to work!
:fund &fund-poke [%proj [our %test] %mula %trib `our 1.000.000 s(xact [5 0x0]) '']
:fund &fund-poke [%proj [our %test] %mula %plej our 50.000.000 6 (crip "{
} plej")]
:fund &fund-poke [%proj [our %test] %draw 0 [7 0x0]]
+:fund &fund-poke [%proj [our %test] %redo ~ ~]
:fund &fund-poke [%proj [our %tes2] %init p(title '5', summary '%', assessment [~nec 1.000.000], currency sepolia-usdc:coin:x, milestones ~[m(title '6', summary '^', cost 1.000.000.000.000)])]
:fund &fund-poke [%proj [our %tes2] %bump %prop ~]
:fund &fund-poke [%prof ~nec %join ~]
diff --git a/meta/exec/regen b/meta/exec/regen
index 4200594..3c075c9 100755
--- a/meta/exec/regen
+++ b/meta/exec/regen
@@ -66,3 +66,6 @@ cp mut/lib/manx-utils.hoon full/lib/
git clone --depth 1 https://github.com/Fang-/suite.git pal
cp pal/mar/pals/effect.hoon full/mar/pals/
cp pal/sur/pals.hoon full/sur/
+
+git clone --depth 1 https://github.com/tloncorp/landscape.git lan
+cp lan/desk/sur/settings.hoon full/sur/