diff --git a/app/package.json b/app/package.json index a3f7b01..204fd8f 100644 --- a/app/package.json +++ b/app/package.json @@ -9,6 +9,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "axios": "^1.6.7", + "buffer": "^6.0.3", "next": "^14.2.13", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 03d8483..2d5e897 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: axios: specifier: ^1.6.7 version: 1.7.2 + buffer: + specifier: ^6.0.3 + version: 6.0.3 next: specifier: ^14.2.13 version: 14.2.13(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2823,6 +2826,12 @@ packages: integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==, } + buffer@6.0.3: + resolution: + { + integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, + } + builtin-status-codes@3.0.0: resolution: { @@ -8023,6 +8032,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builtin-status-codes@3.0.0: {} busboy@1.6.0: diff --git a/app/src/api/clientApi.ts b/app/src/api/clientApi.ts index 8b5f295..3319d62 100644 --- a/app/src/api/clientApi.ts +++ b/app/src/api/clientApi.ts @@ -25,8 +25,51 @@ export interface SendProposalMessageRequest { export interface SendProposalMessageResponse {} +export enum ProposalActionType { + ExternalFunctionCall = 'ExternalFunctionCall', + Transfer = 'Transfer', + SetNumApprovals = 'SetNumApprovals', + SetActiveProposalsLimit = 'SetActiveProposalsLimit', + SetContextValue = 'SetContextValue', + DeleteProposal = 'DeleteProposal', +} + +export type FormActionType = + | 'Cross contract call' + | 'Transfer' + | 'Set context variable' + | 'Change number of approvals needed' + | 'Change number of maximum active proposals'; + +export interface ExternalFunctionCallAction { + type: ProposalActionType.ExternalFunctionCall; + receiver_id: string; + method_name: string; + args: Record; + deposit: string; + gas?: string; +} + +export interface TransferAction { + type: ProposalActionType.Transfer; + amount: string; +} + export interface CreateProposalRequest { - receiver: String; + action_type: string; + params: { + receiver_id?: string; + method_name?: string; + args?: string; + deposit?: string; + gas?: string; + amount?: string; + num_approvals?: number; + active_proposals_limit?: number; + key?: string; + value?: string; + proposal_id?: string; + }; } export interface CreateProposalResponse { @@ -42,8 +85,8 @@ export interface ApproveProposalResponse {} export enum ClientMethod { GET_PROPOSAL_MESSAGES = 'get_proposal_messages', SEND_PROPOSAL_MESSAGE = 'send_proposal_messages', - CREATE_PROPOSAL_MESSAGES = 'create_new_proposal', - APPROVE_PROPOSAL_MESSAGE = 'approve_proposal', + CREATE_PROPOSAL = 'create_new_proposal', + APPROVE_PROPOSAL = 'approve_proposal', } export interface ClientApi { @@ -60,4 +103,5 @@ export interface ClientApi { approveProposal( request: ApproveProposalRequest, ): ApiResponse; + deleteProposal(proposalId: string): ApiResponse; } diff --git a/app/src/api/contractApi.ts b/app/src/api/contractApi.ts index a86944c..8b3b4b5 100644 --- a/app/src/api/contractApi.ts +++ b/app/src/api/contractApi.ts @@ -49,6 +49,7 @@ export interface ContractApi { getContextDetails(contextId: String): ApiResponse; getContextMembers(): ApiResponse; getContextMembersCount(): ApiResponse; + deleteProposal(proposalId: string): ApiResponse; } // async removeProposal(proposalId: String): ApiResponse { diff --git a/app/src/api/dataSource/LogicApiDataSource.ts b/app/src/api/dataSource/LogicApiDataSource.ts index c0ba7b5..6c2a073 100644 --- a/app/src/api/dataSource/LogicApiDataSource.ts +++ b/app/src/api/dataSource/LogicApiDataSource.ts @@ -72,26 +72,51 @@ export class LogicApiDataSource implements ClientApi { return { error }; } - const params: RpcQueryParams = { + console.log('Creating proposal with request:', request); + + const params: RpcQueryParams = { contextId: jwtObject?.context_id ?? getContextId(), - method: ClientMethod.CREATE_PROPOSAL_MESSAGES, - argsJson: request, + method: ClientMethod.CREATE_PROPOSAL, + argsJson: { + request: request, + }, executorPublicKey: jwtObject.executor_public_key, }; - const response = await getJsonRpcClient().execute< - CreateProposalRequest, - CreateProposalResponse - >(params, config); + console.log('RPC params:', params); - if (response?.error) { - return await this.handleError(response.error, {}, this.createProposal); - } + try { + const response = await getJsonRpcClient().execute< + typeof request, + CreateProposalResponse + >(params, config); - return { - data: response.result.output as CreateProposalResponse, - error: null, - }; + console.log('Raw response:', response); + + if (response?.error) { + console.error('RPC error:', response.error); + return await this.handleError(response.error, {}, this.createProposal); + } + + if (!response?.result?.output) { + console.error('Invalid response format:', response); + return { + error: { message: 'Invalid response format', code: 500 }, + data: null, + }; + } + + return { + data: response.result.output as CreateProposalResponse, + error: null, + }; + } catch (err) { + console.error('Unexpected error:', err); + return { + error: { message: err.message || 'Unexpected error', code: 500 }, + data: null, + }; + } } async approveProposal( @@ -106,7 +131,7 @@ export class LogicApiDataSource implements ClientApi { const params: RpcQueryParams = { contextId: jwtObject?.context_id ?? getContextId(), - method: ClientMethod.APPROVE_PROPOSAL_MESSAGE, + method: ClientMethod.APPROVE_PROPOSAL, argsJson: request, executorPublicKey: jwtObject.executor_public_key, }; diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 298496a..7d74c79 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -23,6 +23,9 @@ import { GetProposalMessagesResponse, SendProposalMessageRequest, SendProposalMessageResponse, + ProposalActionType, + ExternalFunctionCallAction, + TransferAction, } from '../../api/clientApi'; import { getContextId, getStorageApplicationId } from '../../utils/node'; import { @@ -33,6 +36,8 @@ import { import { useNavigate } from 'react-router-dom'; import { ContextApiDataSource } from '../../api/dataSource/ContractApiDataSource'; import { ApprovalsCount, ContractProposal } from '../../api/contractApi'; +import { Buffer } from 'buffer'; +import bs58 from 'bs58'; const FullPageCenter = styled.div` display: flex; @@ -156,6 +161,54 @@ const ProposalsWrapper = styled.div` } `; +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +`; + +const ModalContent = styled.div` + background-color: #1e1e1e; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 500px; + color: white; +`; + +const FormGroup = styled.div` + margin-bottom: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + } + + input, + textarea { + width: 100%; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid #444; + background-color: #333; + color: white; + } +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 1rem; +`; + export default function HomePage() { const navigate = useNavigate(); const url = getAppEndpointKey(); @@ -172,6 +225,21 @@ export default function HomePage() { const [approveProposalLoading, setApproveProposalLoading] = useState(false); const [hasAlerted, setHasAlerted] = useState(false); const lastExecutedProposalRef = useRef(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [proposalForm, setProposalForm] = useState({ + actionType: 'Cross contract call', + protocol: 'NEAR', + contractId: '', + methodName: '', + arguments: [{ key: '', value: '' }], + deposit: '', + gas: '', + receiverId: '', + amount: '', + contextVariables: [{ key: '', value: '' }], + minApprovals: '', + maxActiveProposals: '', + }); useEffect(() => { if (!url || !applicationId || !accessToken || !refreshToken) { @@ -212,22 +280,119 @@ export default function HomePage() { } } - async function createProposal() { + async function createProposal(formData: typeof proposalForm) { setCreateProposalLoading(true); - let request: CreateProposalRequest = { - receiver: 'vuki.testnet', - }; + let request: CreateProposalRequest; - const result: ResponseData = - await new LogicApiDataSource().createProposal(request); - if (result?.error) { - console.error('Error:', result.error); - window.alert(`${result.error.message}`); + try { + console.log('Action type:', formData.actionType); + + switch (formData.actionType) { + case 'Cross contract call': { + const argsObject = formData.arguments.reduce( + (acc, curr) => { + if (curr.key && curr.value) { + acc[curr.key] = curr.value; + } + return acc; + }, + {} as Record, + ); + + console.log( + 'Creating ExternalFunctionCall proposal with args:', + argsObject, + ); + + request = { + action_type: 'ExternalFunctionCall', + params: { + receiver_id: formData.contractId, + method_name: formData.methodName, + args: JSON.stringify(argsObject), + deposit: formData.deposit || '0', + gas: + formData.protocol === 'NEAR' + ? formData.gas || '30000000000000' + : '0', + }, + }; + + console.log( + 'Final request structure:', + JSON.stringify(request, null, 2), + ); + break; + } + + case 'Transfer': { + request = { + action_type: ProposalActionType.Transfer, + params: { + receiver_id: formData.receiverId, + amount: formData.amount, + }, + }; + break; + } + + case 'Set context variable': { + request = { + action_type: ProposalActionType.SetContextValue, + params: { + key: formData.contextVariables[0].key, + value: formData.contextVariables[0].value, + }, + }; + break; + } + + case 'Change number of approvals needed': { + request = { + action_type: ProposalActionType.SetNumApprovals, + params: { + num_approvals: Number(formData.minApprovals), + }, + }; + break; + } + + case 'Change number of maximum active proposals': { + request = { + action_type: ProposalActionType.SetActiveProposalsLimit, + params: { + active_proposals_limit: Number(formData.maxActiveProposals), + }, + }; + break; + } + + default: + throw new Error('Invalid action type'); + } + + console.log('Request:', request); + + const result: ResponseData = + await new LogicApiDataSource().createProposal(request); + + if (result?.error) { + console.error('Error:', result.error); + window.alert(`${result.error.message}`); + return; + } + + if (result?.data) { + window.alert(`Proposal created successfully`); + } else { + throw new Error('Invalid response from server'); + } + } catch (error) { + console.error('Error creating proposal:', error); + window.alert(`Error creating proposal: ${error.message}`); + } finally { setCreateProposalLoading(false); - return; } - window.alert(`Proposal with id: ${result.data} created successfully`); - setCreateProposalLoading(false); } const getProposals = async () => { @@ -368,6 +533,39 @@ export default function HomePage() { // observeNodeEvents(); // }, []); + const deleteProposal = async (proposalId: string) => { + try { + // Decode the base58 proposal ID to bytes + const bytes = bs58.decode(proposalId); + // Convert to hex string + const proposalIdHex = Buffer.from(bytes).toString('hex'); + + const request: CreateProposalRequest = { + action_type: ProposalActionType.DeleteProposal, + params: { + proposal_id: proposalIdHex, + }, + }; + const result: ResponseData = + await new LogicApiDataSource().createProposal(request); + + if (result?.error) { + console.error('Error:', result.error); + window.alert(`${result.error.message}`); + return; + } + + if (result?.data) { + window.alert(`Delete proposal created successfully`); + } else { + throw new Error('Invalid response from server'); + } + } catch (error) { + console.error('Error deleting proposal:', error); + window.alert(`Error deleting proposal: ${error.message}`); + } + }; + const logout = () => { clearAppEndpoint(); clearJWT(); @@ -375,6 +573,110 @@ export default function HomePage() { navigate('/auth'); }; + const handleInputChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => { + const { name, value } = e.target; + setProposalForm((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleContextVariableChange = ( + index: number, + field: 'key' | 'value', + value: string, + ) => { + setProposalForm((prev) => { + const newVariables = [...prev.contextVariables]; + newVariables[index] = { + ...newVariables[index], + [field]: value, + }; + return { + ...prev, + contextVariables: newVariables, + }; + }); + }; + + const addContextVariable = () => { + setProposalForm((prev) => ({ + ...prev, + contextVariables: [...prev.contextVariables, { key: '', value: '' }], + })); + }; + + const removeContextVariable = (index: number) => { + setProposalForm((prev) => ({ + ...prev, + contextVariables: prev.contextVariables.filter((_, i) => i !== index), + })); + }; + + const handleArgumentChange = ( + index: number, + field: 'key' | 'value', + value: string, + ) => { + setProposalForm((prev) => { + const newArgs = [...prev.arguments]; + newArgs[index] = { + ...newArgs[index], + [field]: value, + }; + return { + ...prev, + arguments: newArgs, + }; + }); + }; + + const addArgument = () => { + setProposalForm((prev) => ({ + ...prev, + arguments: [...prev.arguments, { key: '', value: '' }], + })); + }; + + const removeArgument = (index: number) => { + setProposalForm((prev) => ({ + ...prev, + arguments: prev.arguments.filter((_, i) => i !== index), + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsModalOpen(false); + await createProposal(proposalForm); + + // Reset form after successful submission + setProposalForm({ + actionType: 'Cross contract call', + protocol: 'NEAR', + contractId: '', + methodName: '', + arguments: [{ key: '', value: '' }], + deposit: '', + gas: '', + receiverId: '', + amount: '', + contextVariables: [{ key: '', value: '' }], + minApprovals: '', + maxActiveProposals: '', + }); + }; + + // Add this helper function to check if current user is the author + const isCurrentUserAuthor = (proposal: ContractProposal): boolean => { + const currentUserKey = getJWTObject()?.executor_public_key; + return proposal.author_id === currentUserKey; + }; + return ( @@ -383,10 +685,281 @@ export default function HomePage() {
Proposals
- + {isModalOpen && ( + setIsModalOpen(false)}> + e.stopPropagation()}> +

Create New Proposal

+
+ + + + + + {proposalForm.actionType === 'Cross contract call' && ( + <> + + + + + + + + + + + + + + + + + {proposalForm.protocol === 'NEAR' && ( + + + + + )} + + + {proposalForm.arguments.map((arg, index) => ( +
+ + + + handleArgumentChange(index, 'key', e.target.value) + } + required + /> + + + + + handleArgumentChange( + index, + 'value', + e.target.value, + ) + } + required + /> + + removeArgument(index)} + style={{ background: '#666', marginBottom: '1rem' }} + > + Remove + +
+ ))} + + Add Argument + +
+ + )} + + {proposalForm.actionType === 'Transfer' && ( + <> + + + + + + + + + + )} + + {proposalForm.actionType === 'Set context variable' && ( + <> +
+ + + + handleContextVariableChange(0, 'key', e.target.value) + } + required + /> + + + + + handleContextVariableChange( + 0, + 'value', + e.target.value, + ) + } + required + /> + +
+ + )} + + {proposalForm.actionType === + 'Change number of approvals needed' && ( + + + + + )} + + {proposalForm.actionType === + 'Change number of maximum active proposals' && ( + + + + + )} + + + setIsModalOpen(false)} + style={{ background: '#666' }} + > + Cancel + + Create Proposal + +
+
+
+ )} +

Number of proposals:

@@ -478,6 +1051,15 @@ export default function HomePage() { > Send Message + {isCurrentUserAuthor(selectedProposal) && ( + { + deleteProposal(selectedProposal.id); + }} + > + Delete Proposal + + )}
)} diff --git a/logic/Cargo.lock b/logic/Cargo.lock index 270fc13..bdacce5 100644 --- a/logic/Cargo.lock +++ b/logic/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "block-buffer" @@ -52,7 +52,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "calimero-sdk" version = "0.1.0" -source = "git+https://github.com/calimero-network/core?branch=master#55c6121c755f3a12efc69c1ae0ceaf67af898f30" +source = "git+https://github.com/calimero-network/core?branch=master#664abd97b09f92241cc74184d340a578ad1b491c" dependencies = [ "borsh", "bs58", @@ -65,7 +65,7 @@ dependencies = [ [[package]] name = "calimero-sdk-macros" version = "0.1.0" -source = "git+https://github.com/calimero-network/core?branch=master#55c6121c755f3a12efc69c1ae0ceaf67af898f30" +source = "git+https://github.com/calimero-network/core?branch=master#664abd97b09f92241cc74184d340a578ad1b491c" dependencies = [ "prettyplease", "proc-macro2", @@ -77,7 +77,7 @@ dependencies = [ [[package]] name = "calimero-storage" version = "0.1.0" -source = "git+https://github.com/calimero-network/core?branch=master#55c6121c755f3a12efc69c1ae0ceaf67af898f30" +source = "git+https://github.com/calimero-network/core?branch=master#664abd97b09f92241cc74184d340a578ad1b491c" dependencies = [ "borsh", "calimero-sdk", @@ -95,7 +95,7 @@ dependencies = [ [[package]] name = "calimero-storage-macros" version = "0.1.0" -source = "git+https://github.com/calimero-network/core?branch=master#55c6121c755f3a12efc69c1ae0ceaf67af898f30" +source = "git+https://github.com/calimero-network/core?branch=master#664abd97b09f92241cc74184d340a578ad1b491c" dependencies = [ "borsh", "quote", @@ -116,9 +116,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hex" @@ -201,14 +201,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "increment" -version = "0.1.0" -dependencies = [ - "calimero-sdk", - "calimero-storage", -] - [[package]] name = "indenter" version = "0.3.3" @@ -217,9 +209,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -227,15 +219,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "memchr" @@ -279,13 +271,23 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] +[[package]] +name = "proxy-contract-demo" +version = "0.1.0" +dependencies = [ + "calimero-sdk", + "calimero-storage", + "hex", + "serde_json", +] + [[package]] name = "quote" version = "1.0.37" @@ -333,18 +335,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -353,9 +355,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -376,9 +378,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -445,9 +447,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "version_check" diff --git a/logic/Cargo.toml b/logic/Cargo.toml index f7e741e..3c8ca85 100644 --- a/logic/Cargo.toml +++ b/logic/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "increment" -description = "Calimero increment/decrement application" +name = "proxy-contract-demo" +description = "Calimero proxy contract interaction demo" version = "0.1.0" edition = "2021" @@ -10,6 +10,8 @@ crate-type = ["cdylib"] [dependencies] calimero-sdk = { git = "https://github.com/calimero-network/core", branch = "master" } calimero-storage = { git = "https://github.com/calimero-network/core", branch = "master" } +hex = "0.4.3" +serde_json = "1.0.133" [profile.app-release] inherits = "release" diff --git a/logic/src/lib.rs b/logic/src/lib.rs index d610add..20bc80c 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -1,9 +1,8 @@ -use calimero_sdk::app; use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; -use calimero_sdk::env; use calimero_sdk::env::ext::{AccountId, ProposalId}; use calimero_sdk::serde::{Deserialize, Serialize}; use calimero_sdk::types::Error; +use calimero_sdk::{app, env}; use calimero_storage::collections::{UnorderedMap, Vector}; #[app::state(emits = Event)] @@ -32,6 +31,13 @@ pub enum Event { ApprovedProposal { id: ProposalId }, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "calimero_sdk::serde")] +pub struct CreateProposalRequest { + pub action_type: String, + pub params: serde_json::Value, +} + #[app::logic] impl AppState { #[app::init] @@ -41,22 +47,123 @@ impl AppState { } } - pub fn create_new_proposal(&mut self, receiver: String) -> Result { - let account_id = AccountId(receiver); - - let amount = 1_000_000_000_000_000_000_000; - - let proposal_id = Self::external() - .propose() - .transfer(account_id, amount) - .send(); + pub fn create_new_proposal( + &mut self, + request: CreateProposalRequest, + ) -> Result { + env::log("Starting create_new_proposal"); + env::log(&format!("Request type: {}", request.action_type)); + + let proposal_id = match request.action_type.as_str() { + "ExternalFunctionCall" => { + env::log("Processing ExternalFunctionCall"); + let receiver_id = request.params["receiver_id"] + .as_str() + .ok_or_else(|| Error::msg("receiver_id is required"))?; + let method_name = request.params["method_name"] + .as_str() + .ok_or_else(|| Error::msg("method_name is required"))?; + let args = request.params["args"] + .as_str() + .ok_or_else(|| Error::msg("args is required"))?; + let deposit = request.params["deposit"] + .as_str() + .ok_or_else(|| Error::msg("deposit is required"))? + .parse::()?; + let gas = request.params["gas"] + .as_str() + .map(|g| g.parse::()) + .transpose()? + .unwrap_or(30_000_000_000_000); + + env::log(&format!( + "Parsed values: receiver_id={}, method_name={}, args={}, deposit={}, gas={}", + receiver_id, method_name, args, deposit, gas + )); + + Self::external() + .propose() + .external_function_call( + receiver_id.to_string(), + method_name.to_string(), + args.to_string(), + deposit, + gas, + ) + .send() + } + "Transfer" => { + env::log("Processing Transfer"); + let receiver_id = request.params["receiver_id"] + .as_str() + .ok_or_else(|| Error::msg("receiver_id is required"))?; + let amount = request.params["amount"] + .as_str() + .ok_or_else(|| Error::msg("amount is required"))? + .parse::()?; + + Self::external() + .propose() + .transfer(AccountId(receiver_id.to_string()), amount) + .send() + } + "SetContextValue" => { + env::log("Processing SetContextValue"); + let key = request.params["key"] + .as_str() + .ok_or_else(|| Error::msg("key is required"))? + .as_bytes() + .to_vec() + .into_boxed_slice(); + let value = request.params["value"] + .as_str() + .ok_or_else(|| Error::msg("value is required"))? + .as_bytes() + .to_vec() + .into_boxed_slice(); + + Self::external() + .propose() + .set_context_value(key, value) + .send() + } + "SetNumApprovals" => Self::external() + .propose() + .set_num_approvals( + request.params["num_approvals"] + .as_u64() + .ok_or(Error::msg("num_approvals is required"))? as u32, + ) + .send(), + "SetActiveProposalsLimit" => Self::external() + .propose() + .set_active_proposals_limit( + request.params["active_proposals_limit"] + .as_u64() + .ok_or(Error::msg("active_proposals_limit is required"))? + as u32, + ) + .send(), + "DeleteProposal" => Self::external() + .propose() + .delete(ProposalId( + hex::decode( + request.params["proposal_id"] + .as_str() + .ok_or_else(|| Error::msg("proposal_id is required"))?, + )? + .try_into() + .map_err(|_| Error::msg("Invalid proposal ID length"))?, + )) + .send(), + _ => return Err(Error::msg("Invalid action type")), + }; env::emit(&Event::ProposalCreated { id: proposal_id }); let old = self.messages.insert(proposal_id, Vector::new())?; - if old.is_some() { - return Err(Error::msg("proposal already exists??")); + return Err(Error::msg("proposal already exists")); } Ok(proposal_id)