web3.db-fileconnector
Version:
GraphQL System Built on web3 technologies. Web3.DB is an open database built on top of web3 technologies. It is a decentralized, open-source database that allows users to store and query data in a secure and efficient manner. The system is designed to be
504 lines (452 loc) • 14.6 kB
JavaScript
import { Connection, PublicKey } from "@solana/web3.js";
import logger from "../../logger/index.js";
export default class SolanaCredentials {
/**
* This will initialize all of the hooks used by this plugin.
* A plugin can register multiple hooks, each hook being linked to a function that will be executed when the hook is triggered
*/
async init() {
// Initialize connection to the Solana cluster using the custom RPC endpoint
this.connection = new Connection(this.rpc_url);
//this.createModel();
return {
HOOKS: {
validate: (stream) => this.isValid(stream),
},
ROUTES: {
POST: {
mint: (req, res) => this.mintCredentials(req, res),
},
},
};
}
async createModel() {
const modelDefinition = {
name: "light_credentials",
version: "1.0",
accountRelation: {
type: "list",
},
schema: {
type: "object",
$schema: "https://json-schema.org/draft/2020-12/schema",
properties: {
receiver: {
type: "string",
},
id: {
type: "string",
},
slug: {
type: "string",
},
name: {
type: "string",
},
protocol: {
type: "string",
},
count: {
type: "number",
},
},
additionalProperties: false,
},
};
let result =
await global.indexingService.ceramic.orbisdb.ceramic.createModel(
modelDefinition
);
logger.debug("result new model:", result);
}
/** Load all transactions from a user */
async getTransactions(address) {
let allPrograms = {};
// Fetch the transaction signatures for the address
const transactions = await this.connection.getSignaturesForAddress(
new PublicKey(address)
);
if (transactions) {
logger.debug(address + " had " + transactions.length + " transactions.");
for (const tx of transactions) {
// Retrieve programs used by this transaction
let { programs } = await this.interpretTransactionDetails(tx.signature);
programs.forEach((program) => {
if (allPrograms[program.id]) {
allPrograms[program.id].count += 1;
} else {
allPrograms[program.id] = { ...program, count: 1 };
}
});
}
}
return allPrograms;
}
// Will interpret the transaction details to identify which programs are being used by this transction
async interpretTransactionDetails(transactionSignature) {
let programsUsed = [];
let signer;
try {
const transactionDetails = await this.connection.getTransaction(
transactionSignature,
{
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
}
);
if (!transactionDetails) {
logger.debug("Transaction not found");
return [];
}
logger.debug("transactionDetails:", transactionDetails);
// Retrieve signer
signer = this.getInitiatingAddress(transactionDetails);
// Retrieve instructions and account keys
const { compiledInstructions } = transactionDetails.transaction.message;
const { staticAccountKeys } = transactionDetails.transaction.message;
compiledInstructions.forEach((instr, index) => {
// Resolve the program ID using the programIdIndex to find it within the staticAccountKeys
const programId = staticAccountKeys[instr.programIdIndex].toString();
// Retrieve program details
let programDetails = solPrograms[programId];
if (programDetails) {
programsUsed.push({
id: programId,
...programDetails,
});
} else {
logger.debug("Unknown program used: ", programId);
}
// We could add some additional logic here to interpret the instruction data based on the program ID
});
} catch (error) {
logger.error("Error fetching transaction:", error);
}
// Filter for uniqueness
const uniquePrograms = programsUsed.filter(
(program, index, self) =>
index === self.findIndex((t) => t.id === program.id)
);
// Return all programs used by this transaction
return {
signer,
programs: uniquePrograms,
};
}
// Retrieve the transaction initiator
getInitiatingAddress(transaction) {
// Check if there are any signatures
if (
transaction &&
transaction.signatures &&
transaction.signatures.length > 0
) {
// The first signature is typically the initiator's
const initiatorSignature = transaction.signatures[0];
// To get the address, we look at the first account in the staticAccountKeys array
// that should be required to be a signer (based on the header).
if (transaction.transaction.message.header.numRequiredSignatures > 0) {
const initiatorAddress =
transaction.transaction.message.staticAccountKeys[0];
return initiatorAddress;
} else {
return null;
}
} else {
return null;
}
}
/** Will retrieve the credentials for a Solana address and mint all of those */
async mintCredentials(req, res) {
if (!req.body) {
return res.badRequest("No request body found.");
}
let streams = [];
const { did } = req.body;
let address = getAddress(did);
/** Retrieve credentials from the user */
let credentials = await this.getTransactions(address);
if (credentials) {
let credentialsArr = Object.values(credentials);
logger.debug("credentialsArr:", credentialsArr);
/** Compute XP score */
let xpScore = this.computeXPScore(credentialsArr);
logger.debug("xpScore:", xpScore);
await this.upsertScore(did, xpScore);
/** Mint new credentials */
for (const credential of credentialsArr) {
let result = await this.upsertCredential(did, credential);
streams.push(result);
}
}
return {
address,
credentials,
streams,
};
}
// Function to compute XP score based on credentials
computeXPScore(credentials) {
let xpScore = 0; // Initialize XP score
let protocolCounts = {}; // Object to track counts of each protocol used
/** Compute initial XP score and track protocol usage */
credentials.forEach((credential) => {
if (protocolCounts[credential.protocol]) {
protocolCounts[credential.protocol] += credential.count;
} else {
protocolCounts[credential.protocol] = credential.count;
}
// Initial XP calculation without multipliers
xpScore += credential.count;
});
/** Apply multipliers based on usage frequency */
Object.keys(protocolCounts).forEach((protocol) => {
const count = protocolCounts[protocol];
if (count > 10) {
xpScore += count * 2; // Double the XP for this protocol
} else if (count > 5) {
xpScore += count * 1.5; // Multiply by 1.5 for this protocol
}
});
/** Calculate total XP before applying unique protocol bonuses */
let totalXP = xpScore;
/** Apply bonuses based on the number of unique protocols used */
const uniqueProtocols = Object.keys(protocolCounts).length;
if (uniqueProtocols >= 20) {
totalXP *= 3; // 200% bonus
} else if (uniqueProtocols >= 10) {
totalXP *= 2; // 100% bonus
} else if (uniqueProtocols >= 5) {
totalXP *= 1.5; // 50% bonus
} else if (uniqueProtocols >= 3) {
totalXP *= 1.1; // 10% bonus
}
return totalXP;
}
/** Mint the score */
async upsertScore(did, xp) {
let stream;
try {
stream = global.indexingService.ceramic.orbisdb
.insert(
"kjzl6hvfrbw6c88zjg5ep2zjj09vag9e3xtp48bo4jce2mvkef9117e0wbwxqp3"
)
.value({
receiver: did,
xp: xp,
})
.context(this.context)
.run();
logger.debug("stream:", stream);
} catch (e) {
logger.error("Stream:", e);
}
}
/** Mint a credential for this user or update the existing one */
async upsertCredential(did, credential) {
let stream;
let model =
"kjzl6hvfrbw6c8g3qm4nwr2ifl43mfy6ik2e44ikw27ryoos7i1ttde24tcm4s8";
// Step 1: Retrieve credential from db (to check if already exists)
let existingCredential;
try {
existingCredential = await global.indexingService.ceramic.orbisdb
.select()
.from(model)
.where({ receiver: did, slug: credential.slug })
.context(this.context)
.orderBy(["indexed_at", "desc"])
.limit(1)
.run();
} catch (e) {
logger.error("Error retrieving credential: ", e);
}
// Build content
let content = {
receiver: did,
...credential,
};
// Step 2: Insert or update
if (existingCredential && existingCredential.rows.length > 0) {
let cred = existingCredential.rows[0];
// Update existing credential
logger.debug("Should update existing credential:", cred);
if (credential.count > cred.count) {
logger.debug("Credential has changed, we UPDATE it.");
stream = await global.indexingService.ceramic.orbisdb
.update(cred.stream_id)
.replace(content)
.run();
} else {
logger.debug("Credential hasn't changed, we don't update it.");
}
} else {
// Insert a new credential
try {
stream = await global.indexingService.ceramic.orbisdb
.insert(model)
.value(content)
.context(this.context)
.run();
} catch (e) {
logger.error("error inserting stream:", e);
}
}
logger.debug("stream:", stream);
return stream;
}
/** Will mark al of the streams as valid */
isValid() {
return true;
}
/** Returns a simple hello:world key value pair which will be added to the plugins_data field */
async hello(stream) {
return {
hello: "world",
};
}
/** Example of an API route returning a simple json object. The routes declared by a plugin are automatically exposed by the OrbisDB instance */
helloApi(req, res) {
res.json({
hello: "world",
});
}
/** Example of an API route returning a simple HTML page. The routes declared by a plugin are automatically exposed by the OrbisDB instance */
async helloHtmlApi(req, res) {
res.type("text/html");
return `<!DOCTYPE html>
<html>
<head>
<title>Simple Page</title>
</head>
<body>
<h1>Hello, this is a simple HTML page</h1>
<p>More content here...</p>
</body>
</html>`;
}
}
const solPrograms = {
dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH: {
name: "Drift Protocol V2",
slug: "drift-v2",
protocol: "drift",
},
M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K: {
name: "Magic Eden V2",
slug: "magic-eden-v2",
protocol: "magic-eden",
},
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: {
name: "Orca",
slug: "orca",
protocol: "orca",
},
JUP6i4ozu5ydDCnLiMogSckDPpbtr7BJ4FtzYWkb5Rk: {
name: "Jupiter v1",
slug: "jupiter-v1",
protocol: "jupiter",
},
JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo: {
name: "Jupiter v2",
slug: "jupiter-v2",
protocol: "jupiter",
},
JUP3c2Uh3WA4Ng34tw6kPd2G4C5BB21Xo36Je1s32Ph: {
name: "Jupiter v3",
slug: "jupiter-v3",
protocol: "jupiter",
},
JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB: {
name: "Jupiter v4",
slug: "jupiter-v4",
protocol: "jupiter",
},
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: {
name: "Jupiter v6",
slug: "jupiter-v6",
protocol: "jupiter",
},
PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY: {
name: "Phoenix",
slug: "phoenix",
protocol: "phoenix",
},
zDEXqXEG7gAyxb1Kg9mK5fPnUdENCGKzWrM21RMdWRq: {
name: "Zeta",
slug: "zeta",
protocol: "zeta",
},
So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo: {
name: "Solend",
slug: "solend",
protocol: "solend",
},
EewxydAPCCVuNEyrVN68PuSYdQ7wKn27V9Gjeoi8dy3S: {
name: "Lifinity Swap",
slug: "lifinity-swap-v1",
protocol: "lifinity",
},
"2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c": {
name: "Lifinity Swap V2",
slug: "lifinity-swap-v2",
protocol: "lifinity",
},
CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK: {
name: "Raydium Concentrated Liquidity",
slug: "raydium-concentrated-liquidity",
protocol: "raydium",
},
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": {
name: "Raydium Liquidity Pool V4",
slug: "raydium-liquidity-pool-v4",
protocol: "raydium",
},
SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr: {
name: "Saros",
slug: "saros",
protocol: "saros",
},
hadeK9DLv9eA7ya5KCTqSvSvRZeJC3JgD5a9Y3CNbvu: {
name: "Hadeswap",
slug: "hadeswap",
protocol: "hadeswap",
},
TSWAPaqyCSx2KABk68Shruf4rp7CxcNi8hAsbdwmHbN: {
name: "Tensor Swap",
slug: "tensor-swap",
protocol: "tensor",
},
TCMPhJdwDryooaGtiocG1u3xcYbRpiJzb283XfCZsDp: {
name: "Tensor cNFT",
slug: "tensor-cnft",
protocol: "tensor",
},
NeonVMyRX5GbCrsAHnUwx1nYYoJAtskU1bWUo6JGNyG: {
name: "Neon EVM",
slug: "neon-evm",
protocol: "neon",
},
jCebN34bUfdeUYJT13J1yG16XWQpt5PDx6Mse9GUqhR: {
name: "Solana Name Service",
slug: "solana-name-service",
protocol: "solana-name-service",
},
"3parcLrT7WnXAcyPfkCz49oofuuf2guUKkjuFkAhZW8Y": {
name: "Parcl",
slug: "parcl",
protocol: "parcl",
},
KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD: {
name: "Kamino Lending",
slug: "kamino-lending",
protocol: "kamino",
},
};
/** Will extract the address from the did */
export function getAddress(did) {
// Split the DID string into an array using ':' as the delimiter
const parts = did.split(":");
// Return the last element of the array
return parts[parts.length - 1];
}