multisig rhai flow POC app

This commit is contained in:
timurgordon
2025-05-20 22:08:00 +03:00
parent 795c04fc5a
commit 123dfc606c
16 changed files with 4021 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flowbroker - Create Flow</title>
<link rel="stylesheet" href="/static/style.css">
<style>
.step, .requirement {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
background-color: #f9f9f9;
}
.step h3, .step h4, .requirement h5 {
margin-top: 0;
}
.step .requirementsContainer {
margin-left: 20px;
border-left: 2px solid #007bff;
padding-left: 15px;
}
button.removeStepBtn, button.removeRequirementBtn {
background-color: #dc3545;
margin-top: 5px;
}
button.removeStepBtn:hover, button.removeRequirementBtn:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<h1>Create New Flow</h1>
<form id="createFlowForm" action="/flows" method="post">
<div>
<label for="flow_name">Flow Name:</label>
<input type="text" id="flow_name" name="flow_name" required>
</div>
<hr>
<div id="stepsContainer">
<!-- Steps will be added here by JavaScript -->
</div>
<button type="button" id="addStepBtn" class="addBtn">Add Step</button>
<hr>
<button type="submit">Create Flow</button>
</form>
<p><a href="/">Back to Flows List</a></p>
<!-- Template for a new step -->
<template id="stepTemplate">
<div class="step" data-step-index="">
<h3>Step <span class="step-number"></span></h3>
<button type="button" class="removeStepBtn">Remove This Step</button>
<div>
<label>Step Description (Optional):</label>
<input type="text" name="steps[X].description" class="step-description">
</div>
<h4>Signature Requirements for Step <span class="step-number"></span></h4>
<div class="requirementsContainer" data-step-index="">
<!-- Requirements will be added here -->
</div>
<button type="button" class="addRequirementBtn addBtn" data-step-index="">Add Signature Requirement</button>
</div>
</template>
<!-- Template for a new signature requirement -->
<template id="requirementTemplate">
<div class="requirement" data-req-index="">
<h5>Requirement <span class="req-number"></span></h5>
<button type="button" class="removeRequirementBtn">Remove Requirement</button>
<div>
<label>Message to Sign:</label>
<textarea name="steps[X].requirements[Y].message" rows="2" required class="req-message"></textarea>
</div>
<div>
<label>Required Public Key:</label>
<input type="text" name="steps[X].requirements[Y].public_key" required class="req-pubkey">
</div>
</div>
</template>
<script>
document.addEventListener('DOMContentLoaded', () => {
const stepsContainer = document.getElementById('stepsContainer');
const addStepBtn = document.getElementById('addStepBtn');
const stepTemplate = document.getElementById('stepTemplate');
const requirementTemplate = document.getElementById('requirementTemplate');
const form = document.getElementById('createFlowForm');
const updateIndices = () => {
const steps = stepsContainer.querySelectorAll('.step');
steps.forEach((step, stepIdx) => {
// Update step-level attributes and text
step.dataset.stepIndex = stepIdx;
step.querySelector('.step-number').textContent = stepIdx + 1;
step.querySelector('.step-description').name = `steps[${stepIdx}].description`;
const addReqBtn = step.querySelector('.addRequirementBtn');
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
const requirements = step.querySelectorAll('.requirementsContainer .requirement');
requirements.forEach((req, reqIdx) => {
// Update requirement-level attributes and text
req.dataset.reqIndex = reqIdx;
req.querySelector('.req-number').textContent = reqIdx + 1;
req.querySelector('.req-message').name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
req.querySelector('.req-pubkey').name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
});
});
};
const addRequirement = (currentStepElement, stepIndex) => {
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer');
const reqFragment = requirementTemplate.content.cloneNode(true);
const newRequirement = reqFragment.querySelector('.requirement');
requirementsContainer.appendChild(newRequirement);
updateIndices(); // Update all indices after adding
};
const addStep = () => {
const stepFragment = stepTemplate.content.cloneNode(true);
const newStep = stepFragment.querySelector('.step');
stepsContainer.appendChild(newStep);
// Add at least one requirement to the new step automatically
const currentStepIndex = stepsContainer.querySelectorAll('.step').length - 1;
addRequirement(newStep, currentStepIndex);
updateIndices(); // Update all indices after adding
};
// Event delegation for remove buttons and add requirement button
stepsContainer.addEventListener('click', (event) => {
if (event.target.classList.contains('removeStepBtn')) {
event.target.closest('.step').remove();
if (stepsContainer.querySelectorAll('.step').length === 0) { // Ensure at least one step
addStep();
}
updateIndices();
} else if (event.target.classList.contains('addRequirementBtn')) {
const stepElement = event.target.closest('.step');
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement(stepElement, stepIndex);
} else if (event.target.classList.contains('removeRequirementBtn')) {
const requirementElement = event.target.closest('.requirement');
const stepElement = event.target.closest('.step');
const requirementsContainer = stepElement.querySelector('.requirementsContainer');
requirementElement.remove();
// Ensure at least one requirement per step
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement(stepElement, stepIndex);
}
updateIndices();
}
});
addStepBtn.addEventListener('click', addStep);
// Add one step by default when the page loads
if (stepsContainer.children.length === 0) {
addStep();
}
// Optional: Validate that there's at least one step and one requirement before submit
form.addEventListener('submit', (event) => {
if (stepsContainer.querySelectorAll('.step').length === 0) {
alert('Please add at least one step to the flow.');
event.preventDefault();
return;
}
const steps = stepsContainer.querySelectorAll('.step');
for (let i = 0; i < steps.length; i++) {
if (steps[i].querySelectorAll('.requirementsContainer .requirement').length === 0) {
alert(`Step ${i + 1} must have at least one signature requirement.`);
event.preventDefault();
return;
}
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flowbroker - Flows</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>Active Flows</h1>
<a href="/flows/new">Create New Flow</a>
<div id="flows-list">
{% if flows %}
<ul>
{% for flow in flows %}
<li>
<strong>{{ flow.name }}</strong> (UUID: {{ flow.flow_uuid }}) - Status: {{ flow.status }}
<br>
Created: {{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }} <!-- Assuming created_at is a Unix timestamp -->
<p><a href="/flows/{{ flow.flow_uuid }}">View Details</a></p> <!-- Link uses flow_uuid -->
</li>
{% endfor %}
</ul>
{% else %}
<p>No active flows. <a href="/flows/new">Create one?</a></p>
{% endif %}
</div>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Flow from Rhai Script</title>
<link rel="stylesheet" href="/static/style.css">
<style>
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f9;
color: #333;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
textarea {
width: 100%;
min-height: 300px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 15px;
font-family: monospace;
font-size: 14px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.back-link {
display: block;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">&larr; Back to Flow List</a>
<h1>Create Flow from Rhai Script</h1>
<div id="rhai_script_examples_data" style="display: none;">
{% for example in example_scripts %}
<div id="rhai_example_content_{{ loop.index }}">{{ example.content }}</div>
{% endfor %}
</div>
<div>
<label for="example_script_selector">Load Example Script:</label>
<select id="example_script_selector">
<option value="">-- Select an Example --</option>
{% for example in example_scripts %}
<option value="{{ example.name }}" data-example-id="rhai_example_content_{{ loop.index }}">{{ example.name }}</option>
{% endfor %}
</select>
</div>
<form action="/flows/create_script" method="POST" style="margin-top: 15px;">
<div>
<label for="rhai_script">Rhai Script:</label>
</div>
<div>
<textarea id="rhai_script" name="rhai_script" placeholder="Enter your Rhai script here or select an example above..."></textarea>
</div>
<button type="submit">Create Flow</button>
</form>
<script>
document.getElementById('example_script_selector').addEventListener('change', function() {
var selectedOption = this.options[this.selectedIndex];
var exampleId = selectedOption.getAttribute('data-example-id');
if (exampleId) {
var scriptContent = document.getElementById(exampleId).textContent; // Use textContent
document.getElementById('rhai_script').value = scriptContent;
} else {
document.getElementById('rhai_script').value = '';
}
});
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,8 @@
// Minimal Single Signature Flow
let flow_id = create_flow("Quick Sign");
let step1_id = add_step(flow_id, "Sign the message", 0);
add_requirement(step1_id, "any_signer_pk", "Please provide your signature.");
print("Minimal Flow (ID: " + flow_id + ") defined.");
()

View File

@@ -0,0 +1,18 @@
// Flow with Multi-Requirement Step
// If create_flow, add_step, or add_requirement fail from Rust,
// the script will stop and the error will be reported by the server.
let flow_id = create_flow("Multi-Req Sign Off");
let step1_id = add_step(flow_id, "Initial Signatures (3 needed)", 0);
add_requirement(step1_id, "signer1_pk", "Signatory 1: Please sign terms.");
add_requirement(step1_id, "signer2_pk", "Signatory 2: Please sign terms.");
add_requirement(step1_id, "signer3_pk", "Signatory 3: Please sign terms.");
let step2_id = add_step(flow_id, "Final Confirmation", 1);
add_requirement(step2_id, "final_approver_pk", "Final approval for multi-req sign off.");
print("Multi-Requirement Flow (ID: " + flow_id + ") defined.");
()

View File

@@ -0,0 +1,14 @@
// Simple Two-Step Flow
// If create_flow, add_step, or add_requirement fail from Rust,
// the script will stop and the error will be reported by the server.
let flow_id = create_flow("Simple Two-Stepper");
let step1_id = add_step(flow_id, "Collect Document", 0);
add_requirement(step1_id, "user_pubkey_document", "Please sign the document hash.");
let step2_id = add_step(flow_id, "Approval Signature", 1);
add_requirement(step2_id, "approver_pubkey", "Please approve the collected document.");
print("Simple Two-Step Flow (ID: " + flow_id + ") defined.");
()